mirror of
https://github.com/woutdp/live_svelte
synced 2026-05-24 09:28:21 +00:00
chore: improved docs. removed live json
This commit is contained in:
parent
a70774e138
commit
09d353e883
48 changed files with 3351 additions and 2110 deletions
|
|
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Removed
|
||||
|
||||
- **live_json** — The `live_json` hex dependency and `live_json_props` feature have been removed from the library and example project. Use built-in props diffing and JSON Patch (see Configuration) for efficient prop updates instead.
|
||||
|
||||
## 0.17.4 - 2026-02-18
|
||||
|
||||
### Added
|
||||
|
|
|
|||
38
README.md
38
README.md
|
|
@ -26,7 +26,6 @@ Svelte inside Phoenix LiveView with seamless end-to-end reactivity
|
|||
- ⭐ **Svelte Preprocessing** Support with [svelte-preprocess](https://github.com/sveltejs/svelte-preprocess)
|
||||
- 🦄 **Tailwind** Support
|
||||
- 💀 **Dead View** Support
|
||||
- 🤏 **live_json** Support
|
||||
- 🦥 **Slot Interoperability**
|
||||
- 📘 **TypeScript** — client assets in TypeScript; public API is type-safe with exported type definitions for consumers
|
||||
|
||||
|
|
@ -734,42 +733,7 @@ end
|
|||
|
||||
Phoenix.HTML.Form, Ecto.Changeset, and Phoenix LiveView upload structs have built-in encoders. Date/Time types are encoded as ISO8601 strings.
|
||||
|
||||
### live_json
|
||||
|
||||
LiveSvelte has support for [live_json](https://github.com/Miserlou/live_json).
|
||||
|
||||
By default, LiveSvelte sends your entire json object over the wire through LiveView. This can be expensive if your json object is big and changes frequently.
|
||||
|
||||
`live_json` on the other hand allows you to only send a _diff_ of the json to Svelte. This is very useful the bigger your json objects get.
|
||||
|
||||
Counterintuitively, you don't always want to use `live_json`. Sometimes it's cheaper to send your entire object again. Although diffs are small, they do add a little bit of data to your json. So if your json is relatively small, I'd recommend not using `live_json`, but it's something to experiment with for your use-case.
|
||||
|
||||
#### Usage
|
||||
|
||||
1. Install [live_json](https://github.com/Miserlou/live_json#installation)
|
||||
|
||||
2. Use `live_json` in your project with LiveSvelte. For example:
|
||||
|
||||
```elixir
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.svelte name="Component" live_json_props={%{my_prop: @ljmy_prop}} socket={@socket} />
|
||||
"""
|
||||
end
|
||||
|
||||
def mount(_, _, socket) do
|
||||
# Get `my_big_json_object` somehow
|
||||
{:ok, LiveJson.initialize("my_prop", my_big_json_object)}
|
||||
end
|
||||
|
||||
def handle_info(%Broadcast{event: "update", payload: my_big_json_object}, socket) do
|
||||
{:noreply, LiveJson.push_patch(socket, "my_prop", my_big_json_object)}
|
||||
end
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
You can find an example [here](https://github.com/woutdp/live_svelte/blob/master/example_project/lib/example_web/live/live_json.ex).
|
||||
For efficient updates when only some props change, use the built-in **props diffing** (see [Configuration](https://hexdocs.pm/live_svelte/configuration.html)) and JSON Patch support; the former `live_json` integration has been removed.
|
||||
|
||||
### Structs and Ecto
|
||||
|
||||
|
|
|
|||
|
|
@ -29,25 +29,9 @@ function getSlots(ref) {
|
|||
return snippets
|
||||
}
|
||||
|
||||
function getLiveJsonProps(ref) {
|
||||
const json = getAttributeJson(ref, "data-live-json")
|
||||
|
||||
// On SSR, data-live-json is the full object we want
|
||||
// After SSR, data-live-json is an array of keys, and we'll get the data from the window
|
||||
if (!Array.isArray(json)) return json
|
||||
|
||||
const liveJsonData = {}
|
||||
for (const liveJsonVariable of json) {
|
||||
const data = window[liveJsonVariable]
|
||||
if (data !== undefined) liveJsonData[liveJsonVariable] = data
|
||||
}
|
||||
return liveJsonData
|
||||
}
|
||||
|
||||
function getProps(ref) {
|
||||
return {
|
||||
...getAttributeJson(ref, "data-props"),
|
||||
...getLiveJsonProps(ref),
|
||||
...getSlots(ref),
|
||||
live: ref,
|
||||
}
|
||||
|
|
@ -89,9 +73,7 @@ function update_state(ref) {
|
|||
else state[key] = payload[key]
|
||||
}
|
||||
}
|
||||
// Always keep live ref, liveJson, and slots in sync
|
||||
const liveJson = getLiveJsonProps(ref)
|
||||
for (const key in liveJson) state[key] = liveJson[key]
|
||||
// Always keep live ref and slots in sync
|
||||
const slots = getSlots(ref)
|
||||
for (const key in slots) state[key] = slots[key]
|
||||
state.live = ref
|
||||
|
|
@ -125,11 +107,6 @@ export function getHooks(components) {
|
|||
const Component = components[componentName]
|
||||
if (!Component) throw new Error(`Unable to find ${componentName} component.`)
|
||||
|
||||
for (const liveJsonElement of Object.keys(getAttributeJson(this, "data-live-json"))) {
|
||||
window.addEventListener(`${liveJsonElement}_initialized`, (_event) => update_state(this), false)
|
||||
window.addEventListener(`${liveJsonElement}_patched`, (_event) => update_state(this), false)
|
||||
}
|
||||
|
||||
// Mount into the inner phx-update="ignore" div so LiveView's DOM
|
||||
// patching won't destroy Svelte's rendered content on server updates.
|
||||
const target = this.el.querySelector("[data-svelte-target]")
|
||||
|
|
|
|||
1759
assets/package-lock.json
generated
1759
assets/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^2.1.0",
|
||||
"esbuild": "^0.24.0",
|
||||
"esbuild-svelte": "^0.9.0",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"esbuild": "^0.27.3",
|
||||
"esbuild-svelte": "^0.9.4",
|
||||
"jsdom": "^28.1.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vitest": "^2.1.0"
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"dependencies": {
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
|
|
|
|||
|
|
@ -1,37 +1,98 @@
|
|||
# Example
|
||||
# LiveSvelte Example Project
|
||||
|
||||
To start your Phoenix server:
|
||||
A working Phoenix application demonstrating LiveSvelte features — Svelte 5 components
|
||||
integrated with Phoenix LiveView.
|
||||
|
||||
- Run `mix setup` to install and setup dependencies
|
||||
- Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
|
||||
## Setup
|
||||
|
||||
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
|
||||
1. Install Elixir + Node.js 19+
|
||||
2. Run `mix setup` (installs deps + npm packages + creates DB)
|
||||
3. Start server: `mix phx.server`
|
||||
4. Visit: http://localhost:4000
|
||||
|
||||
## Demo Categories
|
||||
|
||||
### Basics
|
||||
- **Hello World** (`/hello-world`) — Simplest component rendering
|
||||
- **Struct Props** (`/live-struct`) — Passing Elixir structs as props (requires `@derive Jason.Encoder`)
|
||||
- **Lodash** (`/lodash`) — Using npm packages in Svelte components
|
||||
|
||||
### Interactive
|
||||
- **Counter** (`/live-simple-counter`) — Server state + client events
|
||||
- **Plus/Minus (Live)** (`/live-plus-minus`) — LiveView event handling
|
||||
- **Hybrid Counter** (`/live-plus-minus-hybrid`) — Mix of client and server events
|
||||
- **Lights** (`/live-lights`) — Multiple components sharing LiveView state
|
||||
- **Sigil** (`/live-sigil`) — Inline Svelte templates with the `~V` sigil
|
||||
|
||||
### Data & Real-Time
|
||||
- **Streams** (`/streams`) — Phoenix `stream()` for efficient list updates
|
||||
- **Props Diff** (`/live-props-diff`) — Only changed assigns sent on update (JSON Patch)
|
||||
- **ID List Diff** (`/live-id-list-diff`) — ID-based list diffing for minimal ops
|
||||
- **Chat** (`/live-chat`) — Real-time updates with PubSub + `pushEvent`
|
||||
- **Log List** (`/live-log-list`) — Dynamic list updates
|
||||
- **Breaking News** (`/live-breaking-news`) — Real-time ticker with `~V` sigil
|
||||
|
||||
### Slots
|
||||
- **Simple Slots** (`/live-slots-simple`) — Basic slot usage
|
||||
- **Dynamic Slots** (`/live-slots-dynamic`) — Named slots with dynamic content
|
||||
|
||||
### Composables
|
||||
- **Form** (`/live-form`) — `useLiveForm()` with Ecto changeset validation
|
||||
- **File Upload** (`/live-upload`) — `useLiveUpload()` with progress and validation
|
||||
- **Navigation** (`/live-navigation`) — `useLiveNavigation()` for patch/navigate
|
||||
- **Composition** (`/live-composition`) — `useLiveSvelte()` for pushEvent in component trees
|
||||
- **Event Reply** (`/live-event-reply`) — `useEventReply()` for request-response
|
||||
|
||||
### Advanced
|
||||
- **SSR Demo** (`/live-ssr`) — Server-side rendering with NodeJS (see SSR section below)
|
||||
- **Client Loading** (`/live-client-side-loading`) — Loading slot shown until hydration
|
||||
|
||||
### Ecto
|
||||
- **Notes OTP** (`/live-notes-otp`) — SQLite-backed notes with Ecto
|
||||
|
||||
## Testing
|
||||
|
||||
- Run all tests: `mix test` (by default this excludes E2E tests; use `mix test --only e2e` to run browser tests).
|
||||
- Run only E2E (browser) tests: `mix test --only e2e`
|
||||
|
||||
E2E test modules must use `@moduletag :e2e`. E2E tests use [Wallaby](https://hexdocs.pm/wallaby) and serve the app from **built** assets (`priv/static`). After changing Svelte (or other frontend) code, rebuild before E2E so tests see your changes:
|
||||
|
||||
```bash
|
||||
mix assets.js && mix test --only e2e
|
||||
# Server-side tests (fast, no browser)
|
||||
mix test --only phoenix_test
|
||||
|
||||
# Browser E2E tests (requires ChromeDriver)
|
||||
mix test --only e2e
|
||||
|
||||
# All tests
|
||||
mix test
|
||||
```
|
||||
|
||||
Or use the alias (builds assets then runs tests; pass `--only e2e` to run only E2E):
|
||||
|
||||
**After changing JS/Svelte files**, rebuild before running tests:
|
||||
```bash
|
||||
mix test.e2e --only e2e
|
||||
mix assets.js && mix test
|
||||
```
|
||||
|
||||
You need **Chrome** and **ChromeDriver** on your `PATH` for E2E. If Chromedriver is not installed, run `mix test --exclude e2e` to run the rest of the suite.
|
||||
E2E tests require Chrome + ChromeDriver in PATH. Install with your OS package manager.
|
||||
See `live_svelte/CLAUDE.md` for detailed testing guidance.
|
||||
|
||||
Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
|
||||
## SSR (Server-Side Rendering)
|
||||
|
||||
SSR is disabled by default in development. To enable:
|
||||
|
||||
**config/dev.exs:**
|
||||
```elixir
|
||||
config :live_svelte, ssr: true, ssr_module: LiveSvelte.SSR.ViteJS
|
||||
```
|
||||
|
||||
**For production**, use NodeJS SSR (already configured):
|
||||
```elixir
|
||||
config :live_svelte, ssr: true, ssr_module: LiveSvelte.SSR.NodeJS
|
||||
```
|
||||
|
||||
Build the SSR bundle before SSR works:
|
||||
```bash
|
||||
mix assets.js && mix compile
|
||||
```
|
||||
|
||||
The SSR demo page (`/live-ssr`) uses `ssr={true}` on the component. NodeJS must be available for SSR in production; in test env SSR is disabled globally via `config :live_svelte, ssr: false`.
|
||||
|
||||
## Learn more
|
||||
|
||||
- Official website: https://www.phoenixframework.org/
|
||||
- Guides: https://hexdocs.pm/phoenix/overview.html
|
||||
- Docs: https://hexdocs.pm/phoenix
|
||||
- Forum: https://elixirforum.com/c/phoenix-forum
|
||||
- Source: https://github.com/phoenixframework/phoenix
|
||||
- LiveSvelte: https://github.com/woutdp/live_svelte
|
||||
- Phoenix: https://hexdocs.pm/phoenix
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import "phoenix_html"
|
|||
import {Socket} from "phoenix"
|
||||
import {LiveSocket} from "phoenix_live_view"
|
||||
import topbar from "../vendor/topbar"
|
||||
// TODO(Epic 9): remove createLiveJsonHooks once live_json dependency is removed
|
||||
import {createLiveJsonHooks} from "live_json"
|
||||
import {getHooks} from "live_svelte"
|
||||
import Components from "virtual:live-svelte-components"
|
||||
|
||||
|
|
@ -49,7 +47,6 @@ const PropsDiffPayloadDisplay = {
|
|||
}
|
||||
|
||||
const Hooks = {
|
||||
...createLiveJsonHooks(),
|
||||
...getHooks(Components),
|
||||
PropsDiffPayloadDisplay,
|
||||
}
|
||||
|
|
|
|||
520
example_project/assets/package-lock.json
generated
520
example_project/assets/package-lock.json
generated
|
|
@ -5,52 +5,47 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"live_json": "file:../deps/live_json",
|
||||
"live_svelte": "file:../..",
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html",
|
||||
"phoenix_live_view": "file:../deps/phoenix_live_view"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@types/lodash": "^4.14.192",
|
||||
"daisyui": "^5.0.0",
|
||||
"dynamic-marquee": "^2.6.2",
|
||||
"lodash": "^4.17.21",
|
||||
"stylus": "^0.55.0",
|
||||
"svelte": "^5.19.8",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^6.0.0"
|
||||
"svelte": "^5.53.7",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
},
|
||||
"../..": {
|
||||
"version": "0.17.4",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"prettier": "3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.7",
|
||||
"svelte": "^5.1.13"
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
"svelte": "^5.53.7"
|
||||
}
|
||||
},
|
||||
"../deps/live_json": {
|
||||
"version": "0.4.3",
|
||||
"license": "MIT"
|
||||
},
|
||||
"../deps/phoenix": {
|
||||
"version": "1.8.4",
|
||||
"version": "1.8.5",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.28.6",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@eslint/js": "^9.28.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@stylistic/eslint-plugin": "^5.0.0",
|
||||
"documentation": "^14.0.3",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-plugin-jest": "29.12.1",
|
||||
"eslint": "10.0.2",
|
||||
"eslint-plugin-jest": "29.15.0",
|
||||
"jest": "^30.0.0",
|
||||
"jest-environment-jsdom": "^30.0.0",
|
||||
"jsdom": "^27.0.0",
|
||||
"jsdom": "^28.1.0",
|
||||
"mock-socket": "^9.3.1"
|
||||
}
|
||||
},
|
||||
|
|
@ -82,24 +77,10 @@
|
|||
"phoenix": "1.7.21"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
|
@ -114,9 +95,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -131,9 +112,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -148,9 +129,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -165,9 +146,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -182,9 +163,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -199,9 +180,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -216,9 +197,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -233,9 +214,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
||||
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
|
||||
"integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -250,9 +231,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -267,9 +248,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
|
@ -284,9 +265,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
||||
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
|
||||
"integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
|
|
@ -301,9 +282,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
||||
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
|
||||
"integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
|
|
@ -318,9 +299,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
||||
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
|
||||
"integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
|
@ -335,9 +316,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
||||
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
|
||||
"integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
|
|
@ -352,9 +333,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
||||
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
|
||||
"integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
|
|
@ -369,9 +350,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -386,9 +367,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -403,9 +384,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -420,9 +401,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -437,9 +418,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -454,9 +435,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -471,9 +452,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -488,9 +469,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
||||
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
|
||||
"integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -505,9 +486,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
||||
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
|
||||
"integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
|
@ -522,9 +503,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
||||
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
|
||||
"integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -539,18 +520,25 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
|
|
@ -563,16 +551,6 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
|
|
@ -581,9 +559,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -941,96 +919,56 @@
|
|||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz",
|
||||
"integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==",
|
||||
"node_modules/@sveltejs/acorn-typescript": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz",
|
||||
"integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte": {
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz",
|
||||
"integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
|
||||
"debug": "^4.4.1",
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
||||
"deepmerge": "^4.3.1",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.30.17",
|
||||
"vitefu": "^1.0.6"
|
||||
"magic-string": "^0.30.21",
|
||||
"obug": "^2.1.0",
|
||||
"vitefu": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22"
|
||||
"node": "^20.19 || ^22.12 || >=24"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0",
|
||||
"vite": "^6.0.0"
|
||||
"vite": "^6.3.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz",
|
||||
"integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz",
|
||||
"integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.7"
|
||||
"obug": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22"
|
||||
"node": "^20.19 || ^22.12 || >=24"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0-next.0",
|
||||
"svelte": "^5.0.0",
|
||||
"vite": "^6.0.0"
|
||||
"vite": "^6.3.0 || ^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte-inspector/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte-inspector/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
|
|
@ -1045,10 +983,17 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -1059,20 +1004,10 @@
|
|||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-typescript": {
|
||||
"version": "1.4.13",
|
||||
"resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz",
|
||||
"integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"acorn": ">=8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
|
|
@ -1199,6 +1134,13 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
|
||||
"integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dynamic-marquee": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/dynamic-marquee/-/dynamic-marquee-2.6.4.tgz",
|
||||
|
|
@ -1207,13 +1149,12 @@
|
|||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||
"version": "0.27.3",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
|
||||
"integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
|
|
@ -1221,32 +1162,32 @@
|
|||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.25.12",
|
||||
"@esbuild/android-arm": "0.25.12",
|
||||
"@esbuild/android-arm64": "0.25.12",
|
||||
"@esbuild/android-x64": "0.25.12",
|
||||
"@esbuild/darwin-arm64": "0.25.12",
|
||||
"@esbuild/darwin-x64": "0.25.12",
|
||||
"@esbuild/freebsd-arm64": "0.25.12",
|
||||
"@esbuild/freebsd-x64": "0.25.12",
|
||||
"@esbuild/linux-arm": "0.25.12",
|
||||
"@esbuild/linux-arm64": "0.25.12",
|
||||
"@esbuild/linux-ia32": "0.25.12",
|
||||
"@esbuild/linux-loong64": "0.25.12",
|
||||
"@esbuild/linux-mips64el": "0.25.12",
|
||||
"@esbuild/linux-ppc64": "0.25.12",
|
||||
"@esbuild/linux-riscv64": "0.25.12",
|
||||
"@esbuild/linux-s390x": "0.25.12",
|
||||
"@esbuild/linux-x64": "0.25.12",
|
||||
"@esbuild/netbsd-arm64": "0.25.12",
|
||||
"@esbuild/netbsd-x64": "0.25.12",
|
||||
"@esbuild/openbsd-arm64": "0.25.12",
|
||||
"@esbuild/openbsd-x64": "0.25.12",
|
||||
"@esbuild/openharmony-arm64": "0.25.12",
|
||||
"@esbuild/sunos-x64": "0.25.12",
|
||||
"@esbuild/win32-arm64": "0.25.12",
|
||||
"@esbuild/win32-ia32": "0.25.12",
|
||||
"@esbuild/win32-x64": "0.25.12"
|
||||
"@esbuild/aix-ppc64": "0.27.3",
|
||||
"@esbuild/android-arm": "0.27.3",
|
||||
"@esbuild/android-arm64": "0.27.3",
|
||||
"@esbuild/android-x64": "0.27.3",
|
||||
"@esbuild/darwin-arm64": "0.27.3",
|
||||
"@esbuild/darwin-x64": "0.27.3",
|
||||
"@esbuild/freebsd-arm64": "0.27.3",
|
||||
"@esbuild/freebsd-x64": "0.27.3",
|
||||
"@esbuild/linux-arm": "0.27.3",
|
||||
"@esbuild/linux-arm64": "0.27.3",
|
||||
"@esbuild/linux-ia32": "0.27.3",
|
||||
"@esbuild/linux-loong64": "0.27.3",
|
||||
"@esbuild/linux-mips64el": "0.27.3",
|
||||
"@esbuild/linux-ppc64": "0.27.3",
|
||||
"@esbuild/linux-riscv64": "0.27.3",
|
||||
"@esbuild/linux-s390x": "0.27.3",
|
||||
"@esbuild/linux-x64": "0.27.3",
|
||||
"@esbuild/netbsd-arm64": "0.27.3",
|
||||
"@esbuild/netbsd-x64": "0.27.3",
|
||||
"@esbuild/openbsd-arm64": "0.27.3",
|
||||
"@esbuild/openbsd-x64": "0.27.3",
|
||||
"@esbuild/openharmony-arm64": "0.27.3",
|
||||
"@esbuild/sunos-x64": "0.27.3",
|
||||
"@esbuild/win32-arm64": "0.27.3",
|
||||
"@esbuild/win32-ia32": "0.27.3",
|
||||
"@esbuild/win32-x64": "0.27.3"
|
||||
}
|
||||
},
|
||||
"node_modules/esm-env": {
|
||||
|
|
@ -1257,9 +1198,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.3.tgz",
|
||||
"integrity": "sha512-Xddc1RsoFJ4z9nR7W7BFaEPIp4UXoeQ0+077UdWLxbafMQFyU79sQJMk7kxNgRwQ9/aVgaKacCHC2pUACGwmYw==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz",
|
||||
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -1339,20 +1280,6 @@
|
|||
"@types/estree": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/live_json": {
|
||||
"resolved": "../deps/live_json",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/live_svelte": {
|
||||
"resolved": "../..",
|
||||
"link": true
|
||||
|
|
@ -1433,6 +1360,17 @@
|
|||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/obug": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
|
||||
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/sxzz",
|
||||
"https://opencollective.com/debug"
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
|
|
@ -1627,23 +1565,25 @@
|
|||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.19.8",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.19.8.tgz",
|
||||
"integrity": "sha512-56Vd/nwJrljV0w7RCV1A8sB4/yjSbWW5qrGDTAzp7q42OxwqEWT+6obWzDt41tHjIW+C9Fs2ygtejjJrXR+ZPA==",
|
||||
"version": "5.53.7",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.7.tgz",
|
||||
"integrity": "sha512-uxck1KI7JWtlfP3H6HOWi/94soAl23jsGJkBzN2BAWcQng0+lTrRNhxActFqORgnO9BHVd1hKJhG+ljRuIUWfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
"@types/estree": "^1.0.5",
|
||||
"@types/trusted-types": "^2.0.7",
|
||||
"acorn": "^8.12.1",
|
||||
"acorn-typescript": "^1.4.13",
|
||||
"aria-query": "^5.3.1",
|
||||
"aria-query": "5.3.1",
|
||||
"axobject-query": "^4.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"devalue": "^5.6.3",
|
||||
"esm-env": "^1.2.1",
|
||||
"esrap": "^1.4.3",
|
||||
"esrap": "^2.2.2",
|
||||
"is-reference": "^3.0.3",
|
||||
"locate-character": "^3.0.0",
|
||||
"magic-string": "^0.30.11",
|
||||
|
|
@ -1703,9 +1643,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
|
@ -1717,25 +1657,25 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2",
|
||||
"postcss": "^8.5.3",
|
||||
"rollup": "^4.34.9",
|
||||
"tinyglobby": "^0.2.13"
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.3",
|
||||
"postcss": "^8.5.6",
|
||||
"rollup": "^4.43.0",
|
||||
"tinyglobby": "^0.2.15"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
|
|
@ -1744,14 +1684,14 @@
|
|||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"@types/node": "^20.19.0 || >=22.12.0",
|
||||
"jiti": ">=1.21.0",
|
||||
"less": "*",
|
||||
"less": "^4.0.0",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"sass": "^1.70.0",
|
||||
"sass-embedded": "^1.70.0",
|
||||
"stylus": ">=0.54.8",
|
||||
"sugarss": "^5.0.0",
|
||||
"terser": "^5.16.0",
|
||||
"tsx": "^4.8.1",
|
||||
"yaml": "^2.4.2"
|
||||
|
|
@ -1852,9 +1792,9 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/zimmerframe": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
|
||||
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
|
||||
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
{
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@types/lodash": "^4.14.192",
|
||||
"daisyui": "^5.0.0",
|
||||
"dynamic-marquee": "^2.6.2",
|
||||
"lodash": "^4.17.21",
|
||||
"stylus": "^0.55.0",
|
||||
"svelte": "^5.19.8",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^6.0.0"
|
||||
"svelte": "^5.53.7",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"live_json": "file:../deps/live_json",
|
||||
"live_svelte": "file:../..",
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html",
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
<script>
|
||||
/** @type {{ big_data_set: any }} */
|
||||
let {big_data_set} = $props()
|
||||
|
||||
let keyCount = $derived(Object.keys(big_data_set).length)
|
||||
let byteSize = $derived(JSON.stringify(big_data_set).length)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="text-xs text-base-content/50">Check the WebSocket to see how much data is transferred.</p>
|
||||
<dl class="flex flex-col gap-2">
|
||||
<div class="flex justify-between items-center gap-4 text-sm">
|
||||
<dt class="text-base-content/60">Key length</dt>
|
||||
<dd data-testid="live-json-key-count" class="font-mono font-semibold tabular-nums text-brand">{keyCount.toLocaleString()}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between items-center gap-4 text-sm">
|
||||
<dt class="text-base-content/60">Rough byte size</dt>
|
||||
<dd data-testid="live-json-byte-size" class="font-mono font-semibold tabular-nums text-brand">{byteSize.toLocaleString()}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<button data-testid="live-json-remove-element" phx-click="remove_element" class="btn btn-sm bg-brand text-white border-0 hover:opacity-90 w-fit mt-1"> Remove element </button>
|
||||
</div>
|
||||
17
example_project/assets/svelte/SsrDemo.svelte
Normal file
17
example_project/assets/svelte/SsrDemo.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script>
|
||||
let {greeting} = $props()
|
||||
|
||||
let clicks = $state(0)
|
||||
</script>
|
||||
|
||||
<div class="card bg-base-100 border border-base-300 p-6 max-w-sm text-center space-y-4">
|
||||
<p data-testid="ssr-greeting" class="text-lg">{greeting}</p>
|
||||
<p class="text-sm text-base-content/50">This text was rendered by the server before JavaScript loaded.</p>
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<button data-testid="ssr-increment" class="btn btn-sm bg-brand text-white border-0 hover:opacity-90 w-fit" onclick={() => clicks++}>
|
||||
Click me
|
||||
</button>
|
||||
<span data-testid="click-count" class="font-mono">{clicks}</span>
|
||||
</div>
|
||||
<p class="text-xs text-base-content/30">Click counter is client-side only (not SSR)</p>
|
||||
</div>
|
||||
|
|
@ -1,13 +1,21 @@
|
|||
<script>
|
||||
let {struct} = $props();
|
||||
let {struct} = $props()
|
||||
</script>
|
||||
|
||||
<div class="card bg-base-100 shadow-md border border-base-300/50 overflow-hidden md:min-w-md">
|
||||
<div class="card-body gap-4 p-5">
|
||||
<span class="badge badge-ghost badge-sm font-medium text-base-content/70 w-fit"> Struct </span>
|
||||
<pre data-testid="struct-json" class="font-mono text-sm bg-base-200/80 text-base-content p-4 rounded-lg border border-base-300/50 overflow-x-auto"><code
|
||||
<pre
|
||||
data-testid="struct-json"
|
||||
class="font-mono text-sm bg-base-200/80 text-base-content p-4 rounded-lg border border-base-300/50 overflow-x-auto"><code
|
||||
>{JSON.stringify(struct, null, 2)}</code
|
||||
></pre>
|
||||
<button phx-click="randomize" class="btn btn-sm bg-brand text-white border-0 hover:opacity-90 w-fit" data-testid="struct-randomize-age"> Randomize Age </button>
|
||||
<button
|
||||
phx-click="randomize"
|
||||
class="btn btn-sm bg-brand text-white border-0 hover:opacity-90 w-fit"
|
||||
data-testid="struct-randomize-age"
|
||||
>
|
||||
Randomize Age
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ defmodule ExampleWeb do
|
|||
layouts: [html: ExampleWeb.Layouts]
|
||||
|
||||
import Plug.Conn
|
||||
import ExampleWeb.Gettext
|
||||
use Gettext, backend: ExampleWeb.Gettext
|
||||
|
||||
unquote(verified_routes())
|
||||
end
|
||||
|
|
@ -85,7 +85,7 @@ defmodule ExampleWeb do
|
|||
import Phoenix.HTML
|
||||
# Core UI components and translation
|
||||
import ExampleWeb.CoreComponents
|
||||
import ExampleWeb.Gettext
|
||||
use Gettext, backend: ExampleWeb.Gettext
|
||||
|
||||
import LiveSvelte
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ defmodule ExampleWeb.CoreComponents do
|
|||
Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
|
||||
"""
|
||||
use Phoenix.Component
|
||||
use Gettext, backend: ExampleWeb.Gettext
|
||||
|
||||
alias Phoenix.LiveView.JS
|
||||
import ExampleWeb.Gettext
|
||||
|
||||
@doc """
|
||||
Renders a modal.
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ defmodule ExampleWeb.Layouts do
|
|||
%{label: "Log List", to: ~p"/live-log-list"},
|
||||
%{label: "Breaking News", to: ~p"/live-breaking-news"},
|
||||
%{label: "Chat", to: ~p"/live-chat"},
|
||||
%{label: "LiveJSON", to: ~p"/live-json"},
|
||||
%{label: "Props Diff", to: ~p"/live-props-diff"},
|
||||
%{label: "ID List Diff", to: ~p"/live-id-list-diff"}
|
||||
]
|
||||
|
|
@ -47,7 +46,8 @@ defmodule ExampleWeb.Layouts do
|
|||
%{
|
||||
label: "Advanced",
|
||||
links: [
|
||||
%{label: "Client Loading", to: ~p"/live-client-side-loading"}
|
||||
%{label: "Client Loading", to: ~p"/live-client-side-loading"},
|
||||
%{label: "SSR Demo", to: ~p"/live-ssr"}
|
||||
]
|
||||
},
|
||||
%{
|
||||
|
|
|
|||
|
|
@ -114,12 +114,6 @@
|
|||
</a>
|
||||
<span class="text-base-content/50 text-sm">- PubSub + pushEvent</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/live-json"} class="link link-primary">
|
||||
LiveJSON
|
||||
</a>
|
||||
<span class="text-base-content/50 text-sm">- Efficient JSON diffing</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/live-props-diff"} class="link link-primary">
|
||||
Props Diff
|
||||
|
|
@ -180,6 +174,12 @@
|
|||
</a>
|
||||
<span class="text-base-content/50 text-sm">- Loading states and SSR</span>
|
||||
</li>
|
||||
<li>
|
||||
<a href={~p"/live-ssr"} class="link link-primary">
|
||||
SSR Demo
|
||||
</a>
|
||||
<span class="text-base-content/50 text-sm">- Server-side rendering with NodeJS</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,5 +20,5 @@ defmodule ExampleWeb.Gettext do
|
|||
|
||||
See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
|
||||
"""
|
||||
use Gettext, otp_app: :example
|
||||
use Gettext.Backend, otp_app: :example
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
defmodule ExampleWeb.LiveJson do
|
||||
use ExampleWeb, :live_view
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col justify-center items-center gap-6 p-6">
|
||||
<h2 class="text-center text-2xl font-light my-4">
|
||||
Live JSON
|
||||
</h2>
|
||||
<p class="text-sm text-base-content/50 text-center max-w-md">
|
||||
Large payloads are patched over the wire. Compare SSR vs no-SSR and watch the WebSocket traffic when removing elements.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-8 w-full max-w-4xl">
|
||||
<section class="card bg-base-100 shadow-md border border-base-300/50 overflow-hidden w-full max-w-sm">
|
||||
<div class="card-body gap-2 p-4">
|
||||
<span class="badge badge-outline badge-sm font-medium text-base-content/70 w-fit">
|
||||
SSR
|
||||
</span>
|
||||
<.svelte
|
||||
name="LiveJson"
|
||||
live_json_props={%{big_data_set: @ljbig_data_set}}
|
||||
socket={@socket}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card bg-base-100 shadow-md border border-base-300/50 overflow-hidden w-full max-w-sm">
|
||||
<div class="card-body gap-2 p-4">
|
||||
<span class="badge badge-outline badge-sm font-medium text-base-content/70 w-fit">
|
||||
No SSR
|
||||
</span>
|
||||
<.svelte name="LiveJson" live_json_props={%{big_data_set: @ljbig_data_set}} ssr={false} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def mount(_session, _params, socket) do
|
||||
data =
|
||||
for i <- 1..100_000,
|
||||
into: %{} do
|
||||
{i, Enum.random(1..1_000_000_000)}
|
||||
end
|
||||
|
||||
{:ok, LiveJson.initialize(socket, "big_data_set", data)}
|
||||
end
|
||||
|
||||
def handle_event("remove_element", _values, socket) do
|
||||
random_key =
|
||||
socket.assigns.ljbig_data_set
|
||||
|> Map.keys()
|
||||
|> Enum.random()
|
||||
|
||||
{
|
||||
:noreply,
|
||||
LiveJson.push_patch(
|
||||
socket,
|
||||
"big_data_set",
|
||||
Map.delete(socket.assigns.ljbig_data_set, random_key)
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
20
example_project/lib/example_web/live/live_ssr.ex
Normal file
20
example_project/lib/example_web/live/live_ssr.ex
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
defmodule ExampleWeb.LiveSsr do
|
||||
use ExampleWeb, :live_view
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, assign(socket, greeting: "Hello from the server!")}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-col justify-center items-center gap-6 p-6">
|
||||
<h2 class="text-center text-2xl font-light my-4">SSR Demo</h2>
|
||||
<p class="text-sm text-base-content/50 text-center max-w-md">
|
||||
This component is rendered on the server using NodeJS. The initial HTML includes
|
||||
the Svelte output before the client-side JavaScript runs.
|
||||
</p>
|
||||
<.svelte name="SsrDemo" props={%{greeting: @greeting}} socket={@socket} ssr={true} />
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
@ -32,7 +32,6 @@ defmodule ExampleWeb.Router do
|
|||
live("/live-log-list", LiveLogList)
|
||||
live("/live-breaking-news", LiveBreakingNews)
|
||||
live("/live-chat", LiveChat)
|
||||
live("/live-json", LiveJson)
|
||||
live("/live-props-diff", LivePropsDiff)
|
||||
live("/streams", Streams)
|
||||
live("/live-id-list-diff", LiveIdListDiff)
|
||||
|
|
@ -47,6 +46,7 @@ defmodule ExampleWeb.Router do
|
|||
live("/live-event-reply", LiveEventReply)
|
||||
live("/live-navigation", LiveNavigation)
|
||||
live("/live-navigation/:page", LiveNavigation)
|
||||
live("/live-ssr", LiveSsr)
|
||||
live("/live-composition", LiveComposition)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -36,8 +36,6 @@ defmodule Example.MixProject do
|
|||
{:ecto_sql, "~> 3.12"},
|
||||
{:gettext, "~> 0.20"},
|
||||
{:json_diff_ex, "~> 0.6", override: true},
|
||||
{:jsonpatch, "~> 2.3", override: true},
|
||||
{:live_json, "~> 0.4.5"},
|
||||
{:live_svelte, path: ".."},
|
||||
# {:live_svelte, "~> 0.17.4"},
|
||||
{:phoenix, "~> 1.8.0"},
|
||||
|
|
|
|||
|
|
@ -22,14 +22,13 @@
|
|||
"json_diff_ex": {:hex, :json_diff_ex, "0.7.0", "ef9a7809fce09fecc7fe4ffcac85658ab6de6aaccd55e056dea16da4ecfa6121", [:mix], [], "hexpm", "be71212b736f0f36ef5d805c7d72de4d4e57b5176d0cca99d78eb4d8198da547"},
|
||||
"jsonpatch": {:hex, :jsonpatch, "2.3.1", "49c380f458debbd2bc6e256daeab1081dc89624288f3d77ea83952229388d316", [:make, :mix], [], "hexpm", "06c3e4fff3574cc54d335041f6322fe1b72756e396dd472615ce350d3dd5e758"},
|
||||
"lazy_html": {:hex, :lazy_html, "0.1.8", "677a8642e644eef8de98f3040e2520d42d0f0f8bd6c5cd49db36504e34dffe91", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "0d8167d930b704feb94b41414ca7f5779dff9bca7fcf619fcef18de138f08736"},
|
||||
"live_json": {:hex, :live_json, "0.4.5", "7a97932a8bb944d546a2e0af231aa90889eeaa0ab3f77afc50748636d9202581", [:mix], [{:jason, ">= 1.3.0", [hex: :jason, repo: "hexpm", optional: true]}, {:json_diff_ex, "~> 0.5.0", [hex: :json_diff_ex, repo: "hexpm", optional: false]}, {:jsonpatch, "~> 0.13.1", [hex: :jsonpatch, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 3.1.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.16.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "43f941c94ef3a917f0940552316ae23128b8da274511d1d356494441a69edd71"},
|
||||
"live_svelte": {:hex, :live_svelte, "0.17.2", "b644a22c96e44e03491240e6302fc21cae0c66e2b702531cbbd4732a9f08cc1d", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:nodejs, "~> 3.1", [hex: :nodejs, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 3.3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "17ddf9bca37ddc8e1090dad267cc81f846a8ad8647864e6172fb9fcdd7a0c066"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||
"mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"},
|
||||
"nodejs": {:hex, :nodejs, "3.1.3", "8693fae9fbefa14fb99329292c226df4d4711acfa5a3fa4182dd8d3f779b30bf", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5.1", [hex: :poolboy, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.7", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e7751aad77ac55f8e6c5c07617378afd88d2e0c349d9db2ebb5273aae46ef6a9"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
|
||||
"phoenix": {:hex, :phoenix, "1.8.4", "0387f84f00071cba8d71d930b9121b2fb3645197a9206c31b908d2e7902a4851", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "c988b1cd3b084eebb13e6676d572597d387fa607dab258526637b4e6c4c08543"},
|
||||
"phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
|
||||
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
defmodule ExampleWeb.LiveJsonTest do
|
||||
@moduledoc """
|
||||
E2E test for the LiveJson LiveView (/live-json).
|
||||
Validates that the page mounts with two sections (SSR and No SSR), shows key length
|
||||
and byte size, and that clicking Remove element decreases the key count.
|
||||
"""
|
||||
use ExampleWeb.FeatureCase, async: false
|
||||
|
||||
@moduletag :e2e
|
||||
|
||||
# First key-count dd in the first section (SSR)
|
||||
defp first_key_count_dd(session) do
|
||||
session |> all(Query.css("[data-testid='live-json-key-count']")) |> List.first()
|
||||
end
|
||||
|
||||
defp wait_for_key_count(session, expected, attempts \\ 50) do
|
||||
dd = first_key_count_dd(session)
|
||||
actual = dd && Wallaby.Element.text(dd)
|
||||
|
||||
cond do
|
||||
actual == expected ->
|
||||
session
|
||||
|
||||
attempts == 0 ->
|
||||
raise "timeout waiting for key count #{inspect(expected)}, got #{inspect(actual)}"
|
||||
|
||||
true ->
|
||||
:timer.sleep(100)
|
||||
wait_for_key_count(session, expected, attempts - 1)
|
||||
end
|
||||
end
|
||||
|
||||
test "page mounts and shows heading and description", %{session: session} do
|
||||
session
|
||||
|> visit("/live-json")
|
||||
|> assert_has(Query.css("h2", text: "Live JSON"))
|
||||
|> assert_has(
|
||||
Query.css("p",
|
||||
text:
|
||||
"Large payloads are patched over the wire. Compare SSR vs no-SSR and watch the WebSocket traffic when removing elements."
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
test "renders two sections with key length and Remove element button", %{session: session} do
|
||||
session = visit(session, "/live-json")
|
||||
|
||||
# Two section cards (SSR and No SSR); at least one of each badge
|
||||
ssr_badges = session |> all(Query.css("section.card span.badge", text: "SSR"))
|
||||
no_ssr_badges = session |> all(Query.css("section.card span.badge", text: "No SSR"))
|
||||
assert length(ssr_badges) >= 1
|
||||
assert length(no_ssr_badges) >= 1
|
||||
|
||||
# Wait for Svelte to hydrate and show key count
|
||||
session = wait_for_key_count(session, "100,000")
|
||||
key_length_dts = session |> all(Query.css("dt", text: "Key length"))
|
||||
assert length(key_length_dts) >= 1
|
||||
remove_btns = session |> all(Query.css("[data-testid='live-json-remove-element']"))
|
||||
assert length(remove_btns) >= 1
|
||||
end
|
||||
|
||||
test "clicking Remove element decreases key count", %{session: session} do
|
||||
session =
|
||||
session
|
||||
|> visit("/live-json")
|
||||
|> wait_for_key_count("100,000")
|
||||
|
||||
# Click the first section's Remove element button
|
||||
[remove_btn | _] = session |> all(Query.css("[data-testid='live-json-remove-element']"))
|
||||
Wallaby.Element.click(remove_btn)
|
||||
|
||||
session = wait_for_key_count(session, "99,999")
|
||||
dd = first_key_count_dd(session)
|
||||
assert Wallaby.Element.text(dd) == "99,999"
|
||||
end
|
||||
end
|
||||
24
example_project/test/example_web/live/live_ssr_test.exs
Normal file
24
example_project/test/example_web/live/live_ssr_test.exs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
defmodule ExampleWeb.Live.LiveSsrTest do
|
||||
@moduledoc """
|
||||
E2E test for the LiveSsr LiveView (/live-ssr).
|
||||
Validates that the SSR demo page renders the greeting from props and that
|
||||
the client-side click counter works after hydration.
|
||||
"""
|
||||
use ExampleWeb.FeatureCase, async: false
|
||||
|
||||
@moduletag :e2e
|
||||
|
||||
test "renders SSR greeting and click counter works", %{session: session} do
|
||||
session = visit(session, "/live-ssr")
|
||||
|
||||
session |> find(Query.css("[data-testid='ssr-greeting']", text: "Hello from the server!"))
|
||||
|
||||
count_el = session |> find(Query.css("[data-testid='click-count']"))
|
||||
assert Wallaby.Element.text(count_el) == "0"
|
||||
|
||||
session = session |> click(Query.css("[data-testid='ssr-increment']"))
|
||||
|
||||
count_el = session |> find(Query.css("[data-testid='click-count']"))
|
||||
assert Wallaby.Element.text(count_el) == "1"
|
||||
end
|
||||
end
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
defmodule ExampleWeb.PhoenixTest.LiveJsonTest do
|
||||
@moduledoc """
|
||||
PhoenixTest (in-process) for LiveJson (/live-json).
|
||||
Validates that the page renders two LiveJson sections (SSR and No SSR)
|
||||
and shows key length and Remove element button. Remove-element behavior
|
||||
is covered in E2E (live_json_test.exs).
|
||||
"""
|
||||
use ExampleWeb.ConnCase, async: false
|
||||
import PhoenixTest
|
||||
|
||||
@moduletag :phoenix_test
|
||||
|
||||
setup do
|
||||
ssr = Application.get_env(:live_svelte, :ssr, false)
|
||||
Application.put_env(:live_svelte, :ssr, true)
|
||||
|
||||
on_exit(fn ->
|
||||
Application.put_env(:live_svelte, :ssr, ssr)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
test "renders page heading and description", %{conn: conn} do
|
||||
conn
|
||||
|> visit("/live-json")
|
||||
|> assert_has("h2", text: "Live JSON")
|
||||
|> assert_has("p",
|
||||
text:
|
||||
"Large payloads are patched over the wire. Compare SSR vs no-SSR and watch the WebSocket traffic when removing elements."
|
||||
)
|
||||
end
|
||||
|
||||
test "renders two sections (SSR and No SSR) with LiveJson component", %{conn: conn} do
|
||||
conn
|
||||
|> visit("/live-json")
|
||||
|> assert_has("span.badge", text: "SSR")
|
||||
|> assert_has("span.badge", text: "No SSR")
|
||||
|> assert_has("[data-name='LiveJson']", count: 2)
|
||||
end
|
||||
|
||||
test "shows key length and Remove element button", %{conn: conn} do
|
||||
conn
|
||||
|> visit("/live-json")
|
||||
|> assert_has("dt", text: "Key length")
|
||||
|> assert_has("[data-testid='live-json-remove-element']")
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
defmodule ExampleWeb.LiveSsrTest do
|
||||
use ExampleWeb.ConnCase, async: true
|
||||
@moduletag :phoenix_test
|
||||
|
||||
import PhoenixTest
|
||||
|
||||
test "renders SSR demo page", %{conn: conn} do
|
||||
conn
|
||||
|> visit("/live-ssr")
|
||||
|> assert_has("h2", text: "SSR Demo")
|
||||
|> assert_has("[data-props*='greeting']")
|
||||
end
|
||||
end
|
||||
386
guides/api_reference.md
Normal file
386
guides/api_reference.md
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
# API Reference
|
||||
|
||||
Complete reference for all public LiveSvelte APIs.
|
||||
|
||||
## Elixir API
|
||||
|
||||
### `LiveSvelte.svelte/1`
|
||||
|
||||
Renders a Svelte component in a LiveView template.
|
||||
|
||||
```heex
|
||||
<.svelte name="Counter" props={%{count: @count}} socket={@socket} />
|
||||
```
|
||||
|
||||
**Attributes:**
|
||||
|
||||
| Attribute | Type | Default | Required | Description |
|
||||
|-----------|------|---------|----------|-------------|
|
||||
| `name` | `string` | — | ✓ | Component name (filename without `.svelte`, relative to `assets/svelte/`) |
|
||||
| `props` | `map` | `%{}` | | Props to pass to the component |
|
||||
| `socket` | `map` | `nil` | | LiveView socket — required when `ssr: true` |
|
||||
| `id` | `string` | auto | | Stable DOM id override |
|
||||
| `key` | `any` | `nil` | | Identity key for DOM id generation in loops |
|
||||
| `class` | `string` | `nil` | | CSS class on the wrapper div |
|
||||
| `ssr` | `boolean` | `true` | | Enable SSR for this component |
|
||||
| `diff` | `boolean` | `true` | | Enable props diffing (requires `enable_props_diff: true` in config) |
|
||||
| `:loading` slot | | | | Content shown while component loads (only with `ssr={false}`) |
|
||||
| `:inner_block` slot | | | | Inner content (passed to Svelte as a slot) |
|
||||
|
||||
**Name examples:**
|
||||
```
|
||||
Counter.svelte → name="Counter"
|
||||
forms/UserForm.svelte → name="forms/UserForm"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `~V` Sigil
|
||||
|
||||
Inline Svelte template as a LiveView render macro.
|
||||
|
||||
```elixir
|
||||
def render(assigns) do
|
||||
~V"""
|
||||
<script>
|
||||
let { count } = $props()
|
||||
</script>
|
||||
<p>Count: {count}</p>
|
||||
"""
|
||||
end
|
||||
```
|
||||
|
||||
All LiveView assigns are automatically available as props. The template is written to `assets/svelte/_build/MyModule.svelte` at compile time.
|
||||
|
||||
---
|
||||
|
||||
### `LiveSvelte.Components`
|
||||
|
||||
Auto-generated shorthand component functions based on discovered `.svelte` files.
|
||||
|
||||
```elixir
|
||||
# In web module html_helpers:
|
||||
use LiveSvelte.Components
|
||||
|
||||
# In templates — instead of <.svelte name="Counter" ...>:
|
||||
<.Counter count={@count} socket={@socket} />
|
||||
```
|
||||
|
||||
`Counter.svelte` → `<.Counter>`, `forms/UserForm.svelte` → `<.forms_UserForm>` (slashes converted to underscores).
|
||||
|
||||
---
|
||||
|
||||
### `LiveSvelte.Test.get_svelte/1,2`
|
||||
|
||||
Inspect Svelte component props from HTML in tests.
|
||||
|
||||
```elixir
|
||||
import LiveSvelte.Test
|
||||
|
||||
# Get first component in HTML
|
||||
component = get_svelte(html)
|
||||
|
||||
# Get component by name
|
||||
component = get_svelte(html, name: "Counter")
|
||||
|
||||
# Get component by DOM id
|
||||
component = get_svelte(html, id: "Counter-1")
|
||||
|
||||
# Get directly from a LiveView
|
||||
{:ok, view, _html} = live(conn, "/counter")
|
||||
component = get_svelte(view, name: "Counter")
|
||||
```
|
||||
|
||||
Returns a map with:
|
||||
- `name` — component name string
|
||||
- `id` — DOM id of the wrapper element
|
||||
- `props` — decoded props map (string keys)
|
||||
- `slots` — map of slot name → HTML string
|
||||
- `ssr` — boolean, whether SSR was active
|
||||
|
||||
**Example:**
|
||||
```elixir
|
||||
{:ok, _view, html} = live(conn, "/counter")
|
||||
component = get_svelte(html, name: "Counter")
|
||||
assert component.props["count"] == 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `LiveSvelte.Encoder` Protocol
|
||||
|
||||
Protocol for encoding custom structs as JSON props. Implement it directly or use `@derive`:
|
||||
|
||||
```elixir
|
||||
# Simple derive — exposes all public fields
|
||||
@derive LiveSvelte.Encoder
|
||||
defstruct [:id, :name]
|
||||
|
||||
# Restricted derive — only expose listed fields
|
||||
@derive {LiveSvelte.Encoder, only: [:id, :name, :email]}
|
||||
defstruct [:id, :name, :email, :password_hash]
|
||||
|
||||
# Excluded fields derive
|
||||
@derive {LiveSvelte.Encoder, except: [:password_hash]}
|
||||
defstruct [:id, :name, :email, :password_hash]
|
||||
```
|
||||
|
||||
Without `@derive`, passing a struct as a prop will raise an error.
|
||||
|
||||
---
|
||||
|
||||
### `LiveSvelte.Reload` / `vite_assets/0`
|
||||
|
||||
HMR helper for development. Includes the Vite dev server client script.
|
||||
|
||||
```heex
|
||||
<!-- In root layout, development only -->
|
||||
<%= if Application.get_env(:live_svelte, :ssr_module) == LiveSvelte.SSR.ViteJS do %>
|
||||
<LiveSvelte.Reload.vite_assets path="/assets/js/app.js" />
|
||||
<% end %>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JavaScript API
|
||||
|
||||
### `getHooks(Components)`
|
||||
|
||||
Entry point. Returns a hooks map to pass to `LiveSocket`:
|
||||
|
||||
```ts
|
||||
import { getHooks } from "live_svelte"
|
||||
import Components from "virtual:live-svelte-components"
|
||||
|
||||
const liveSocket = new LiveSocket("/live", Socket, {
|
||||
hooks: getHooks(Components),
|
||||
params: { _csrf_token: csrfToken }
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `useLiveSvelte()`
|
||||
|
||||
Access the Phoenix hook context from any LiveSvelte-mounted component.
|
||||
|
||||
```ts
|
||||
import { useLiveSvelte } from "live_svelte"
|
||||
```
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { useLiveSvelte } from "live_svelte"
|
||||
|
||||
const { pushEvent, pushEventTo, live } = useLiveSvelte()
|
||||
|
||||
function save(data) {
|
||||
pushEvent("save", data)
|
||||
}
|
||||
|
||||
function saveWithReply(data) {
|
||||
pushEvent("save", data, (reply) => console.log(reply))
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
- `live` — raw Phoenix hook context
|
||||
- `pushEvent(event, payload, callback?)` — push event to LiveView
|
||||
- `pushEventTo(target, event, payload, callback?)` — push event to specific LiveView
|
||||
|
||||
---
|
||||
|
||||
### `useLiveEvent(event, callback)`
|
||||
|
||||
Subscribe to a server-sent LiveView event. Automatically cleans up on component destroy.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { useLiveEvent } from "live_svelte"
|
||||
|
||||
useLiveEvent("item_added", (payload) => {
|
||||
console.log("New item:", payload)
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `useLiveConnection()`
|
||||
|
||||
Reactive WebSocket connection state.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { useLiveConnection } from "live_svelte"
|
||||
|
||||
const conn = useLiveConnection()
|
||||
</script>
|
||||
|
||||
{#if !conn.connected}
|
||||
<div class="banner">Reconnecting...</div>
|
||||
{/if}
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
- `connected` — `boolean`, reactive
|
||||
|
||||
---
|
||||
|
||||
### `useLiveNavigation()`
|
||||
|
||||
Client-side LiveView navigation.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { useLiveNavigation } from "live_svelte"
|
||||
|
||||
const { patch, navigate } = useLiveNavigation()
|
||||
</script>
|
||||
|
||||
<button onclick={() => patch("?page=2")}>Next page</button>
|
||||
<button onclick={() => navigate("/other")}>Navigate</button>
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
- `patch(hrefOrParams, opts?)` — patch current LiveView (triggers `handle_params/3`)
|
||||
- `navigate(href, opts?)` — navigate to a new LiveView
|
||||
|
||||
Both accept `{ replace: true }` to use `history.replaceState`.
|
||||
|
||||
---
|
||||
|
||||
### `useLiveForm(formFn, opts?)`
|
||||
|
||||
Reactive form binding with Ecto changeset support. See [Forms and Validation](forms.md) for full documentation.
|
||||
|
||||
```ts
|
||||
import { useLiveForm } from "live_svelte"
|
||||
```
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { useLiveForm } from "live_svelte"
|
||||
let { form } = $props()
|
||||
const { field, fieldArray } = useLiveForm(() => form)
|
||||
</script>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `formFn` — getter function returning the form prop
|
||||
- `opts?` — `{ changeEvent?, submitEvent?, debounceInMilliseconds? }`
|
||||
|
||||
**Returns:**
|
||||
- `field(name)` — field descriptor with `name`, `value`, `error`, `phx-debounce`
|
||||
- `fieldArray(name)` — array field with `fields`, `append`, `prepend`, `remove`
|
||||
|
||||
---
|
||||
|
||||
### `useLiveUpload(uploadConfig, options)`
|
||||
|
||||
File upload integration. See [File Uploads](uploads.md) for full documentation.
|
||||
|
||||
```ts
|
||||
import { useLiveUpload } from "live_svelte"
|
||||
```
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { useLiveUpload } from "live_svelte"
|
||||
let { uploads } = $props()
|
||||
const { showFilePicker, entries, submit, cancel, sync } = useLiveUpload(
|
||||
uploads.avatar,
|
||||
{ changeEvent: "validate", submitEvent: "submit" }
|
||||
)
|
||||
$effect(() => sync(uploads.avatar))
|
||||
</script>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `uploadConfig` — the upload config object (e.g. `uploads.avatar`), passed directly not as a getter
|
||||
- `options` — `{ changeEvent?: string, submitEvent: string }` — `submitEvent` is required
|
||||
|
||||
**Returns:**
|
||||
- `showFilePicker()` — open file picker dialog
|
||||
- `addFiles(files)` — enqueue files from `File[]` or `DataTransfer` (drag-drop)
|
||||
- `entries` — `Readable<UploadEntry[]>` store — use `$entries` in templates
|
||||
- `progress` — `Readable<number>` — overall progress 0–100
|
||||
- `valid` — `Readable<boolean>` — true when no top-level upload errors
|
||||
- `submit()` — programmatic form submit
|
||||
- `cancel(ref?)` — cancel entry by ref string, or all when omitted
|
||||
- `clear()` — reset file input
|
||||
- `sync(config)` — merge updated config from server; call in `$effect`
|
||||
|
||||
---
|
||||
|
||||
### `useEventReply()`
|
||||
|
||||
Request-response pattern: push an event and await a reply.
|
||||
|
||||
```ts
|
||||
import { useEventReply } from "live_svelte"
|
||||
```
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { useEventReply } from "live_svelte"
|
||||
const { push } = useEventReply()
|
||||
|
||||
async function save(data) {
|
||||
const result = await push("save", data)
|
||||
console.log("Server replied:", result)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
- `push(event, payload)` — returns a `Promise` that resolves with the server reply
|
||||
|
||||
The LiveView must reply using `{:reply, payload, socket}` in `handle_event/3`:
|
||||
|
||||
```elixir
|
||||
def handle_event("save", params, socket) do
|
||||
{:reply, %{status: "ok"}, socket}
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `Link` Component
|
||||
|
||||
Client-side navigation component. Svelte equivalent of Phoenix's `<.link>`.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { Link } from "live_svelte"
|
||||
</script>
|
||||
|
||||
<Link href="/other-page">Go to other page</Link>
|
||||
<Link href="/other-page" replace={true}>Replace history</Link>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Telemetry Events
|
||||
|
||||
| Event | Measurements | Metadata | Description |
|
||||
|-------|-------------|----------|-------------|
|
||||
| `[:live_svelte, :ssr, :start]` | `%{system_time: integer}` | `%{component: name}` | SSR render begins |
|
||||
| `[:live_svelte, :ssr, :stop]` | `%{duration_microseconds: integer}` | `%{component: name}` | SSR render completes |
|
||||
| `[:live_svelte, :ssr, :exception]` | `%{system_time: integer}` | `%{component: name, reason: term}` | SSR render fails |
|
||||
|
||||
---
|
||||
|
||||
## Configuration Keys
|
||||
|
||||
See [Configuration](configuration.md) for full details.
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `config :live_svelte, :ssr` | `true` | Global SSR enable/disable |
|
||||
| `config :live_svelte, :ssr_module` | `LiveSvelte.SSR.NodeJS` | SSR module |
|
||||
| `config :live_svelte, :json_library` | `LiveSvelte.JSON` | JSON encoder |
|
||||
| `config :live_svelte, :enable_props_diff` | `true` | Props diffing system |
|
||||
| `config :live_svelte, :gettext_backend` | `nil` | Gettext for form errors |
|
||||
| `config :live_svelte, :vite_host` | `"http://localhost:5173"` | Vite dev server URL |
|
||||
215
guides/basic_usage.md
Normal file
215
guides/basic_usage.md
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# Basic Usage
|
||||
|
||||
This guide covers the fundamentals of using LiveSvelte: the `<.svelte>` component, props, events, and the `~V` sigil.
|
||||
|
||||
## Your First Component
|
||||
|
||||
### 1. Create a Svelte component
|
||||
|
||||
Place Svelte files in `assets/svelte/`. LiveSvelte discovers all `*.svelte` files in that directory at compile time.
|
||||
|
||||
```svelte
|
||||
<!-- assets/svelte/Counter.svelte -->
|
||||
<script>
|
||||
let { count, live } = $props()
|
||||
|
||||
function increment() {
|
||||
live.pushEvent("increment", {})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<p>Count: {count}</p>
|
||||
<button onclick={increment}>Increment</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 2. Use it in a LiveView
|
||||
|
||||
```elixir
|
||||
# lib/my_app_web/live/counter_live.ex
|
||||
defmodule MyAppWeb.CounterLive do
|
||||
use MyAppWeb, :live_view
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, assign(socket, :count, 0)}
|
||||
end
|
||||
|
||||
def handle_event("increment", _params, socket) do
|
||||
{:noreply, update(socket, :count, &(&1 + 1))}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.svelte name="Counter" props={%{count: @count}} socket={@socket} />
|
||||
"""
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
That's it. When the user clicks the button, `pushEvent("increment", {})` sends the event to `handle_event/3`, the count is incremented, and Svelte re-renders automatically.
|
||||
|
||||
## Props
|
||||
|
||||
Pass any JSON-serializable map as `props`:
|
||||
|
||||
```heex
|
||||
<.svelte name="UserCard" props={%{name: @user.name, role: @user.role}} socket={@socket} />
|
||||
```
|
||||
|
||||
In the component, receive with `$props()`:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { name, role } = $props()
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h2>{name}</h2>
|
||||
<span>{role}</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Struct Props
|
||||
|
||||
Structs must implement the `LiveSvelte.Encoder` protocol before being passed as props. Use `@derive` for the default implementation:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.User do
|
||||
@derive {LiveSvelte.Encoder, only: [:id, :name, :email]}
|
||||
defstruct [:id, :name, :email, :password_hash]
|
||||
end
|
||||
```
|
||||
|
||||
The `only:` list controls which fields are exposed. Never derive without `only:` for structs with sensitive fields.
|
||||
|
||||
## The `live` Prop
|
||||
|
||||
LiveSvelte automatically passes a `live` prop to every mounted component. Use it to communicate with the server:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { live } = $props()
|
||||
|
||||
// Push event to server (fire-and-forget)
|
||||
function save(data) {
|
||||
live.pushEvent("save", data)
|
||||
}
|
||||
|
||||
// Push event and receive reply
|
||||
function saveWithReply(data) {
|
||||
live.pushEvent("save", data, (reply) => {
|
||||
console.log("Server replied:", reply)
|
||||
})
|
||||
}
|
||||
|
||||
// Subscribe to server-sent events
|
||||
live.handleEvent("flash", ({ message }) => {
|
||||
alert(message)
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
## Composable Alternative to `live` Prop
|
||||
|
||||
Instead of using the `live` prop, you can use composables which work from any component in the tree — no prop drilling:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { useLiveSvelte, useLiveEvent } from "live_svelte"
|
||||
|
||||
const { pushEvent } = useLiveSvelte()
|
||||
|
||||
useLiveEvent("flash", ({ message }) => {
|
||||
alert(message)
|
||||
})
|
||||
|
||||
function save(data) {
|
||||
pushEvent("save", data)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
See the [API Reference](api_reference.md) for all composables.
|
||||
|
||||
## Component Shorthand with `LiveSvelte.Components`
|
||||
|
||||
Add `use LiveSvelte.Components` to your LiveView (or web module) for shorthand component functions:
|
||||
|
||||
```elixir
|
||||
# In web module html_helpers (added by Igniter installer):
|
||||
import LiveSvelte
|
||||
use LiveSvelte.Components
|
||||
```
|
||||
|
||||
Then instead of `<.svelte name="Counter" ...>`, use:
|
||||
|
||||
```heex
|
||||
<.Counter count={@count} socket={@socket} />
|
||||
```
|
||||
|
||||
The function names are generated from your `.svelte` filenames. `Counter.svelte` → `<.Counter>`, `UserCard.svelte` → `<.UserCard>`.
|
||||
|
||||
> #### `socket` is required for SSR {: .info}
|
||||
>
|
||||
> Always pass `socket={@socket}` when SSR is enabled. It's used to detect the initial dead render vs. connected live render. You can omit it only when `ssr={false}`.
|
||||
|
||||
## Inline Templates with the `~V` Sigil
|
||||
|
||||
For small, one-off components, write Svelte templates inline using the `~V` sigil:
|
||||
|
||||
```elixir
|
||||
def render(assigns) do
|
||||
~V"""
|
||||
<script>
|
||||
let { count } = $props()
|
||||
</script>
|
||||
<p>Count is {count}</p>
|
||||
"""
|
||||
end
|
||||
```
|
||||
|
||||
The sigil writes the template to `assets/svelte/_build/` at compile time and mounts it like any other component. All LiveView assigns are automatically available as props.
|
||||
|
||||
> #### Svelte 5 Syntax Required {: .warning}
|
||||
>
|
||||
> Always use Svelte 5 runes syntax. Do NOT use Svelte 4 patterns:
|
||||
>
|
||||
> | ❌ Svelte 4 | ✅ Svelte 5 |
|
||||
> |-------------|-------------|
|
||||
> | `export let count` | `let { count } = $props()` |
|
||||
> | `let x = 0` (reactive) | `let x = $state(0)` |
|
||||
> | `$: doubled = x * 2` | `let doubled = $derived(x * 2)` |
|
||||
> | `<script context="module">` | module-level code in `.js` files |
|
||||
|
||||
## Local State
|
||||
|
||||
Local component state uses `$state()`:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { items } = $props()
|
||||
|
||||
let filter = $state("")
|
||||
let filtered = $derived(items.filter(i => i.name.includes(filter)))
|
||||
</script>
|
||||
|
||||
<input bind:value={filter} placeholder="Filter..." />
|
||||
|
||||
{#each filtered as item}
|
||||
<li>{item.name}</li>
|
||||
{/each}
|
||||
```
|
||||
|
||||
## Component Discovery
|
||||
|
||||
LiveSvelte scans `assets/svelte/**/*.svelte` at compile time. Component names in `<.svelte name="...">` are relative paths without the `.svelte` extension:
|
||||
|
||||
```
|
||||
assets/svelte/Counter.svelte → name="Counter"
|
||||
assets/svelte/forms/UserForm.svelte → name="forms/UserForm"
|
||||
```
|
||||
|
||||
## phx-update="ignore"
|
||||
|
||||
LiveSvelte automatically sets `phx-update="ignore"` on the component wrapper div, which prevents LiveView from patching Svelte's DOM after mount. All updates flow through the hook. This is required for correct operation — do not override it.
|
||||
130
guides/configuration.md
Normal file
130
guides/configuration.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# Configuration
|
||||
|
||||
All LiveSvelte configuration is set via `Application.put_env(:live_svelte, key, value)` in your config files.
|
||||
|
||||
## Application Config Keys
|
||||
|
||||
| Key | Default | Description |
|
||||
|-----|---------|-------------|
|
||||
| `:ssr` | `true` | Enable server-side rendering globally |
|
||||
| `:ssr_module` | `LiveSvelte.SSR.NodeJS` | SSR module: `NodeJS` or `ViteJS` |
|
||||
| `:json_library` | `LiveSvelte.JSON` | JSON encoder (e.g. `Jason`) |
|
||||
| `:enable_props_diff` | `true` | Enable three-tier props diffing system |
|
||||
| `:gettext_backend` | `nil` | Gettext module for form error translation |
|
||||
| `:vite_host` | `"http://localhost:5173"` | Vite dev server URL (used by ViteJS SSR mode) |
|
||||
|
||||
## Typical Configuration by Environment
|
||||
|
||||
### `config/config.exs` (base)
|
||||
|
||||
```elixir
|
||||
config :live_svelte, ssr: true
|
||||
```
|
||||
|
||||
### `config/dev.exs` (development)
|
||||
|
||||
```elixir
|
||||
config :live_svelte,
|
||||
ssr_module: LiveSvelte.SSR.ViteJS,
|
||||
vite_host: "http://localhost:5173"
|
||||
```
|
||||
|
||||
### `config/prod.exs` (production)
|
||||
|
||||
```elixir
|
||||
config :live_svelte,
|
||||
ssr_module: LiveSvelte.SSR.NodeJS,
|
||||
ssr: true
|
||||
```
|
||||
|
||||
### `config/test.exs` (test)
|
||||
|
||||
```elixir
|
||||
# SSR is off in tests by default (NodeJS not started in test env)
|
||||
config :live_svelte, ssr: false
|
||||
```
|
||||
|
||||
## Per-Component Attributes
|
||||
|
||||
These `<.svelte>` component attributes override global config for individual components:
|
||||
|
||||
| Attribute | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `name` | `string` | **required** | Svelte component filename (without `.svelte`) |
|
||||
| `props` | `map` | `%{}` | Props passed to the component |
|
||||
| `socket` | `map` | `nil` | LiveView socket — required when `ssr: true` |
|
||||
| `id` | `string` | auto | Stable DOM id override |
|
||||
| `key` | `any` | `nil` | Identity key for DOM id in loops |
|
||||
| `class` | `string` | `nil` | CSS class for the wrapper div |
|
||||
| `ssr` | `boolean` | `true` | Enable SSR for this component |
|
||||
| `diff` | `boolean` | `true` | Enable props diffing for this component |
|
||||
|
||||
### Examples
|
||||
|
||||
```heex
|
||||
<!-- Disable SSR for a heavy chart component -->
|
||||
<.svelte name="HeavyChart" props={%{data: @data}} socket={@socket} ssr={false} />
|
||||
|
||||
<!-- Disable props diffing (always send full props) -->
|
||||
<.svelte name="SimpleDisplay" props={%{label: @label}} socket={@socket} diff={false} />
|
||||
|
||||
<!-- Stable id for components in loops -->
|
||||
<.svelte name="Item" props={%{id: item.id, title: item.title}} socket={@socket} key={item.id} />
|
||||
```
|
||||
|
||||
## Vite Plugin Options
|
||||
|
||||
Configure the `liveSveltePlugin` in `assets/vite.config.mjs`:
|
||||
|
||||
```js
|
||||
import { liveSveltePlugin } from "live_svelte/vitePlugin"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
svelte(),
|
||||
liveSveltePlugin({
|
||||
// Options (all optional):
|
||||
paths: ["assets/svelte"], // Directories to scan for .svelte files
|
||||
entrypoint: "assets/js/app.js" // Main app entry point
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### Default Paths
|
||||
|
||||
By default, `liveSveltePlugin` discovers Svelte components in:
|
||||
- `assets/svelte/**/*.svelte`
|
||||
- `lib/**/*.svelte` (for colocated components next to LiveView modules)
|
||||
|
||||
## JSON Library
|
||||
|
||||
By default, LiveSvelte uses its own JSON encoder which handles `LiveSvelte.Encoder` protocol automatically. To use `Jason` instead:
|
||||
|
||||
```elixir
|
||||
config :live_svelte, json_library: Jason
|
||||
```
|
||||
|
||||
When using an external JSON library, LiveSvelte still runs all values through `LiveSvelte.Encoder` before passing to the library.
|
||||
|
||||
## Props Diffing
|
||||
|
||||
Props diffing is enabled by default. To disable globally:
|
||||
|
||||
```elixir
|
||||
config :live_svelte, enable_props_diff: false
|
||||
```
|
||||
|
||||
When disabled, LiveSvelte always sends the full props map on every update. This can be useful for debugging or for very simple UIs where diffing overhead is not worth it.
|
||||
|
||||
The three tiers (change tracking → JSON Patch → ID-based list diffing) are all part of the same system and toggle together.
|
||||
|
||||
## Gettext Integration
|
||||
|
||||
To translate Ecto changeset error messages using your Gettext backend:
|
||||
|
||||
```elixir
|
||||
config :live_svelte, gettext_backend: MyAppWeb.Gettext
|
||||
```
|
||||
|
||||
This affects `useLiveForm` — error messages shown in `field().error` will use translated strings from your `priv/gettext/` directory.
|
||||
163
guides/deployment.md
Normal file
163
guides/deployment.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# Deployment
|
||||
|
||||
Deploying a LiveSvelte application requires Node.js on the server for SSR (server-side rendering). This guide covers the production build process and deployment considerations.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Node.js 19+** on the production server (for `LiveSvelte.SSR.NodeJS`)
|
||||
- Standard Phoenix/Elixir deployment tooling (releases, Docker, etc.)
|
||||
|
||||
## Build Steps
|
||||
|
||||
```bash
|
||||
# 1. Build client bundle and SSR bundle
|
||||
mix assets.js
|
||||
|
||||
# 2. Compile application (copies SSR bundle to _build)
|
||||
mix compile
|
||||
|
||||
# OR in a single release command:
|
||||
MIX_ENV=prod mix assets.js && MIX_ENV=prod mix release
|
||||
```
|
||||
|
||||
### What `mix assets.js` Does
|
||||
|
||||
The `assets.js` alias runs (in order):
|
||||
|
||||
1. `npx vite build` — builds the client JavaScript bundle to `priv/static/assets/`
|
||||
2. `npx vite build --config vite.ssr.config.js` — builds the SSR bundle to `priv/svelte/server.js`
|
||||
3. `tailwind default` — builds CSS to `priv/static/assets/app.css`
|
||||
|
||||
> #### Vite Must Run Before Tailwind {: .warning}
|
||||
>
|
||||
> The Vite client build uses `emptyOutDir: true` and clears `priv/static/assets/` first. Always run Vite before Tailwind, or Tailwind's `app.css` will be deleted. The `mix assets.js` alias handles this order correctly.
|
||||
|
||||
## NodeJS Supervisor
|
||||
|
||||
The Igniter installer adds `NodeJS.Supervisor` to your `application.ex`:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Application do
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
# ... other children ...
|
||||
{NodeJS.Supervisor, [path: LiveSvelte.SSR.NodeJS.server_path(), pool_size: 4]}
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
`LiveSvelte.SSR.NodeJS.server_path/0` returns the path to `priv/svelte/server.js`, which is the SSR bundle.
|
||||
|
||||
Adjust `pool_size` based on expected SSR load. A pool of 4 workers is a reasonable default.
|
||||
|
||||
## Production Config
|
||||
|
||||
```elixir
|
||||
# config/prod.exs
|
||||
config :live_svelte,
|
||||
ssr_module: LiveSvelte.SSR.NodeJS,
|
||||
ssr: true
|
||||
```
|
||||
|
||||
## SSR Bundle
|
||||
|
||||
The SSR bundle (`priv/svelte/server.js`) is:
|
||||
- Built from `assets/vite.ssr.config.js`
|
||||
- Fully self-contained (all dependencies bundled, `ssr: { noExternal: true }`)
|
||||
- Required to be present at application start when `ssr_module: LiveSvelte.SSR.NodeJS`
|
||||
|
||||
After `mix assets.js`, `mix compile` copies `priv/svelte/server.js` into `_build/`. This copy in `_build/` is what NodeJS.Supervisor actually loads at runtime.
|
||||
|
||||
> #### Always Compile After Building SSR Bundle {: .info}
|
||||
>
|
||||
> After `mix assets.js`, run `mix compile` so `_build/` gets the updated SSR bundle. In a CI/CD pipeline, ensure both steps run.
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
Include Node.js in your Docker image:
|
||||
|
||||
```dockerfile
|
||||
# Multi-stage build example
|
||||
FROM node:20-slim AS assets-builder
|
||||
WORKDIR /app
|
||||
COPY assets/ assets/
|
||||
COPY deps/ deps/
|
||||
RUN cd assets && npm install
|
||||
RUN mix assets.js
|
||||
|
||||
FROM elixir:1.17-slim AS release-builder
|
||||
# ... standard Elixir release steps ...
|
||||
RUN mix release
|
||||
|
||||
FROM elixir:1.17-slim
|
||||
# Include Node.js for SSR
|
||||
RUN apt-get update && apt-get install -y nodejs npm
|
||||
COPY --from=release-builder /app/_build/prod/rel/my_app ./
|
||||
CMD ["/app/bin/my_app", "start"]
|
||||
```
|
||||
|
||||
A simpler approach is to use an image that includes both Elixir and Node.js:
|
||||
|
||||
```dockerfile
|
||||
FROM hexpm/elixir:1.17.3-erlang-27.1.2-debian-bookworm-20241016-slim
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
```
|
||||
|
||||
## Disabling SSR in Production
|
||||
|
||||
If you choose not to use SSR (e.g. to avoid Node.js on the server), disable it globally and remove `NodeJS.Supervisor`:
|
||||
|
||||
```elixir
|
||||
# config/prod.exs
|
||||
config :live_svelte, ssr: false
|
||||
```
|
||||
|
||||
Remove `NodeJS.Supervisor` from `application.ex` children. The `{:nodejs, "~> 3.1"}` dependency can remain in `mix.exs` but won't be used.
|
||||
|
||||
## Per-Component SSR Opt-Out
|
||||
|
||||
Even with global SSR enabled, you can disable SSR for expensive components to reduce Node.js load:
|
||||
|
||||
```heex
|
||||
<.svelte name="HeavyVisualization" props={%{data: @data}} socket={@socket} ssr={false} />
|
||||
```
|
||||
|
||||
Use SSR primarily for above-the-fold content where first-paint HTML matters.
|
||||
|
||||
## Telemetry for Observability
|
||||
|
||||
Attach SSR telemetry handlers to monitor production performance:
|
||||
|
||||
```elixir
|
||||
:telemetry.attach_many(
|
||||
"live-svelte-ssr-metrics",
|
||||
[
|
||||
[:live_svelte, :ssr, :stop],
|
||||
[:live_svelte, :ssr, :exception]
|
||||
],
|
||||
fn
|
||||
[:live_svelte, :ssr, :stop], measurements, _meta, _ ->
|
||||
MyApp.Metrics.histogram("live_svelte.ssr.duration", measurements.duration_microseconds)
|
||||
[:live_svelte, :ssr, :exception], _measurements, meta, _ ->
|
||||
Logger.error("SSR failed: #{inspect(meta.reason)}")
|
||||
end,
|
||||
nil
|
||||
)
|
||||
```
|
||||
|
||||
## Upgrading
|
||||
|
||||
When upgrading LiveSvelte versions:
|
||||
|
||||
1. Update `{:live_svelte, "~> x.y"}` in `mix.exs`
|
||||
2. Run `mix deps.get`
|
||||
3. Check `CHANGELOG.md` for breaking changes
|
||||
4. Rebuild: `mix assets.js && mix compile`
|
||||
5. Run tests: `mix test`
|
||||
212
guides/forms.md
Normal file
212
guides/forms.md
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
# Forms and Validation
|
||||
|
||||
LiveSvelte provides `useLiveForm` for building reactive forms backed by Ecto changesets with server-side validation.
|
||||
|
||||
## Quick Example
|
||||
|
||||
**LiveView:**
|
||||
|
||||
```elixir
|
||||
defmodule MyAppWeb.UserFormLive do
|
||||
use MyAppWeb, :live_view
|
||||
alias MyApp.Accounts
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
form = to_form(Accounts.change_user(%Accounts.User{}))
|
||||
{:ok, assign(socket, form: form)}
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"user" => params}, socket) do
|
||||
form = params |> Accounts.change_user() |> Map.put(:action, :validate) |> to_form()
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
|
||||
def handle_event("submit", %{"user" => params}, socket) do
|
||||
case Accounts.create_user(params) do
|
||||
{:ok, _user} -> {:noreply, push_navigate(socket, to: "/")}
|
||||
{:error, changeset} -> {:noreply, assign(socket, form: to_form(changeset))}
|
||||
end
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.svelte name="UserForm" props={%{form: @form}} socket={@socket} />
|
||||
"""
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Svelte Component:**
|
||||
|
||||
```svelte
|
||||
<!-- assets/svelte/UserForm.svelte -->
|
||||
<script>
|
||||
import { useLiveForm } from "live_svelte"
|
||||
|
||||
let { form } = $props()
|
||||
|
||||
const { field } = useLiveForm(() => form)
|
||||
</script>
|
||||
|
||||
<form phx-submit="submit" phx-change="validate">
|
||||
<div>
|
||||
<label>Name</label>
|
||||
<input {...field("name")} />
|
||||
{#if field("name").error}
|
||||
<span class="error">{field("name").error}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Email</label>
|
||||
<input type="email" {...field("email")} />
|
||||
{#if field("email").error}
|
||||
<span class="error">{field("email").error}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
## The `useLiveForm` Composable
|
||||
|
||||
```ts
|
||||
import { useLiveForm } from "live_svelte"
|
||||
|
||||
const { field, fieldArray } = useLiveForm(() => form, options?)
|
||||
```
|
||||
|
||||
The first argument is a **getter function** (not the form value directly). This ensures `useLiveForm` always reads the latest reactive prop value.
|
||||
|
||||
### Options
|
||||
|
||||
```ts
|
||||
type FormOptions = {
|
||||
changeEvent?: string // Event name for validation (default: "validate")
|
||||
submitEvent?: string // Event name for submission (default: "submit")
|
||||
debounceInMilliseconds?: number // Debounce delay for change events (default: 300)
|
||||
}
|
||||
```
|
||||
|
||||
## The `field()` Function
|
||||
|
||||
`field(name)` returns an object you can spread onto an `<input>` element:
|
||||
|
||||
```svelte
|
||||
<input {...field("email")} />
|
||||
```
|
||||
|
||||
It returns:
|
||||
- `name` — the HTML input name (matches changeset field)
|
||||
- `value` — current field value from the changeset
|
||||
- `error` — error message string (or `null`)
|
||||
- `phx-debounce` — debounce attribute for change events
|
||||
|
||||
You can also access properties individually:
|
||||
|
||||
```svelte
|
||||
<input
|
||||
name={field("email").name}
|
||||
value={field("email").value}
|
||||
class={field("email").error ? "border-red-500" : ""}
|
||||
/>
|
||||
{#if field("email").error}
|
||||
<p>{field("email").error}</p>
|
||||
{/if}
|
||||
```
|
||||
|
||||
## Nested Fields
|
||||
|
||||
Access nested fields with dot notation:
|
||||
|
||||
```svelte
|
||||
<input {...field("address.street")} />
|
||||
<input {...field("address.city")} />
|
||||
```
|
||||
|
||||
## Dynamic Arrays with `fieldArray()`
|
||||
|
||||
For `embeds_many` or `has_many` with nested forms:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { useLiveForm } from "live_svelte"
|
||||
|
||||
let { form } = $props()
|
||||
const { field, fieldArray } = useLiveForm(() => form)
|
||||
|
||||
const skills = fieldArray("skills")
|
||||
</script>
|
||||
|
||||
{#each skills.fields as skillField, i}
|
||||
<div>
|
||||
<input {...field(`skills.${i}.name`)} />
|
||||
<button type="button" onclick={() => skills.remove(i)}>Remove</button>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<button type="button" onclick={() => skills.append({ name: "" })}>Add Skill</button>
|
||||
```
|
||||
|
||||
`fieldArray(path)` returns:
|
||||
- `fields` — reactive array of field descriptors
|
||||
- `append(value)` — add an item to the end
|
||||
- `prepend(value)` — add an item to the start
|
||||
- `remove(index)` — remove an item by index
|
||||
|
||||
## Encoding Changesets as Props
|
||||
|
||||
To pass a changeset form as props, use `LiveSvelte.Encoder` for the changeset data. Phoenix's `to_form/1` produces a `Phoenix.HTML.Form` struct that LiveSvelte can encode automatically.
|
||||
|
||||
For custom structs used inside the form data, use `@derive`:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Address do
|
||||
@derive {LiveSvelte.Encoder, only: [:street, :city, :zip]}
|
||||
embedded_schema do
|
||||
field :street, :string
|
||||
field :city, :string
|
||||
field :zip, :string
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## TypeScript Types
|
||||
|
||||
```ts
|
||||
import type { Form } from "live_svelte"
|
||||
|
||||
// Type your component props
|
||||
let { form }: { form: Form<{ name: string; email: string }> } = $props()
|
||||
|
||||
const { field } = useLiveForm(() => form)
|
||||
// field("name").value is typed as string
|
||||
```
|
||||
|
||||
## Gettext Integration
|
||||
|
||||
If you have a Gettext backend configured, LiveSvelte translates error messages automatically:
|
||||
|
||||
```elixir
|
||||
# config/config.exs
|
||||
config :live_svelte, gettext_backend: MyAppWeb.Gettext
|
||||
```
|
||||
|
||||
Error messages from changesets will use your Gettext translations.
|
||||
|
||||
## Full Form Example with Validation
|
||||
|
||||
```elixir
|
||||
# LiveView
|
||||
def handle_event("validate", %{"user" => params}, socket) do
|
||||
changeset =
|
||||
%User{}
|
||||
|> User.changeset(params)
|
||||
|> Map.put(:action, :validate)
|
||||
|
||||
{:noreply, assign(socket, form: to_form(changeset))}
|
||||
end
|
||||
```
|
||||
|
||||
Setting `action: :validate` on the changeset causes Ecto to include validation errors, which LiveSvelte then passes back to the `field().error` values in the component.
|
||||
160
guides/installation.md
Normal file
160
guides/installation.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# Installation
|
||||
|
||||
LiveSvelte uses [Vite](https://vitejs.dev/) for both client and SSR builds, replacing the default `esbuild` setup in Phoenix projects.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js 19+** — required for SSR (server-side rendering)
|
||||
- **Elixir 1.17+**
|
||||
- **Phoenix 1.8+** — required for the Igniter installer
|
||||
- **Igniter** — the installation scaffolding tool
|
||||
|
||||
## Quick Start (Recommended)
|
||||
|
||||
### Step 1: Install the Igniter archive
|
||||
|
||||
```bash
|
||||
mix archive.install hex igniter_new
|
||||
```
|
||||
|
||||
### Step 2: Add LiveSvelte to your project
|
||||
|
||||
For an **existing** Phoenix 1.8+ project:
|
||||
|
||||
```bash
|
||||
mix igniter.install live_svelte
|
||||
```
|
||||
|
||||
For a **new** project with LiveSvelte pre-installed:
|
||||
|
||||
```bash
|
||||
mix igniter.new my_app --with phx.new --install live_svelte
|
||||
```
|
||||
|
||||
Use the `--bun` flag to use `bun` instead of `npm`:
|
||||
|
||||
```bash
|
||||
mix igniter.install live_svelte --bun
|
||||
```
|
||||
|
||||
### Step 3: Install JS dependencies and build
|
||||
|
||||
```bash
|
||||
cd assets && npm install && cd ..
|
||||
mix assets.js
|
||||
mix phx.server
|
||||
```
|
||||
|
||||
Visit `/svelte_demo` to verify the installation with the generated demo component.
|
||||
|
||||
## What the Installer Does
|
||||
|
||||
Running `mix igniter.install live_svelte` makes the following changes to your project:
|
||||
|
||||
**`assets/package.json`** — adds:
|
||||
- `live_svelte: "file:../deps/live_svelte"` (dependency)
|
||||
- `svelte: "^5.0.0"` (dev dependency)
|
||||
- `@sveltejs/vite-plugin-svelte` (dev dependency)
|
||||
|
||||
**`assets/vite.config.mjs`** — adds the Svelte plugin and `liveSveltePlugin`:
|
||||
```js
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import { liveSveltePlugin } from "live_svelte/vitePlugin"
|
||||
|
||||
// ...
|
||||
plugins: [svelte(), liveSveltePlugin()],
|
||||
```
|
||||
|
||||
**`assets/vite.ssr.config.js`** — new file for the SSR bundle (Node.js server rendering)
|
||||
|
||||
**`assets/js/app.js`** — adds hook wiring:
|
||||
```js
|
||||
import { getHooks } from "live_svelte"
|
||||
import Components from "virtual:live-svelte-components"
|
||||
|
||||
const liveSocket = new LiveSocket("/live", Socket, {
|
||||
hooks: getHooks(Components),
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
**`lib/app_web.ex`** — adds `import LiveSvelte` to `html_helpers`
|
||||
|
||||
**`lib/app/application.ex`** — adds NodeJS supervisor for production SSR:
|
||||
```elixir
|
||||
{NodeJS.Supervisor, [path: LiveSvelte.SSR.NodeJS.server_path(), pool_size: 4]}
|
||||
```
|
||||
|
||||
**`config/config.exs`** — base SSR config:
|
||||
```elixir
|
||||
config :live_svelte, ssr: true
|
||||
```
|
||||
|
||||
**`config/dev.exs`** — development SSR via Vite dev server:
|
||||
```elixir
|
||||
config :live_svelte,
|
||||
ssr_module: LiveSvelte.SSR.ViteJS,
|
||||
vite_host: "http://localhost:5173"
|
||||
```
|
||||
|
||||
**`config/prod.exs`** — production SSR via NodeJS:
|
||||
```elixir
|
||||
config :live_svelte,
|
||||
ssr_module: LiveSvelte.SSR.NodeJS,
|
||||
ssr: true
|
||||
```
|
||||
|
||||
**`mix.exs`** — adds the `assets.js` alias that runs both Vite builds plus Tailwind:
|
||||
```elixir
|
||||
"assets.js": [
|
||||
"cmd npx vite build",
|
||||
"cmd npx vite build --config vite.ssr.config.js",
|
||||
"tailwind default"
|
||||
]
|
||||
```
|
||||
|
||||
**`assets/svelte/`** — creates the Svelte components directory with a demo component
|
||||
|
||||
**`assets/app.css`** — adds `@source "../svelte";` if Tailwind is present
|
||||
|
||||
**`.gitignore`** — adds `/assets/svelte/_build/` and `/priv/svelte/`
|
||||
|
||||
> #### Phoenix Version Requirement {: .warning}
|
||||
>
|
||||
> The Igniter installer requires **Phoenix 1.8+**. The library itself works with Phoenix 1.7+ when installed manually.
|
||||
|
||||
## Manual Installation
|
||||
|
||||
> #### Manual Installation Not Recommended {: .warning}
|
||||
>
|
||||
> Manual installation steps are complex and kept out-of-date as dependencies evolve. We strongly recommend using `mix igniter.install live_svelte` instead.
|
||||
|
||||
If you must install manually (e.g. Phoenix < 1.8), the overall steps mirror the LiveVue manual installation process:
|
||||
|
||||
1. Add `{:live_svelte, "~> 0.17"}` to `mix.exs` deps
|
||||
2. Configure Vite with the Svelte plugin and `liveSveltePlugin`
|
||||
3. Create `vite.ssr.config.js`
|
||||
4. Wire up `getHooks(Components)` in `app.js`
|
||||
5. Add `import LiveSvelte` to `html_helpers`
|
||||
6. Add `NodeJS.Supervisor` to `application.ex`
|
||||
7. Configure SSR in `config/`
|
||||
|
||||
## Disabling SSR
|
||||
|
||||
If you don't need server-side rendering, disable it globally:
|
||||
|
||||
```elixir
|
||||
# config/config.exs
|
||||
config :live_svelte, ssr: false
|
||||
```
|
||||
|
||||
Or per-component:
|
||||
|
||||
```heex
|
||||
<.svelte name="Counter" props={%{count: @count}} socket={@socket} ssr={false} />
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Basic Usage](basic_usage.md) — your first `<.svelte>` component
|
||||
- [Configuration](configuration.md) — all config options
|
||||
52
guides/introduction.md
Normal file
52
guides/introduction.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Introduction
|
||||
|
||||
LiveSvelte brings end-to-end reactivity between [Phoenix LiveView](https://github.com/phoenixframework/phoenix_live_view) and [Svelte 5](https://svelte.dev/) components. Server state flows directly into Svelte components as reactive props, and user interactions in Svelte components push events back to the LiveView process — all over the existing Phoenix WebSocket.
|
||||
|
||||
## Three-Layer Architecture
|
||||
|
||||
LiveSvelte is built on three layers that work together:
|
||||
|
||||
```
|
||||
LiveView (Elixir) → SvelteHook (Phoenix hook) → Svelte 5 (component)
|
||||
server assigns reads data attrs reactive props
|
||||
handle_event/3 pushEvent/handleEvent $props(), $state()
|
||||
```
|
||||
|
||||
1. **LiveView** renders an HTML wrapper div with JSON-encoded props in `data-props`. State lives on the server.
|
||||
2. **SvelteHook** (a Phoenix LiveView JS hook) mounts and updates Svelte components. It reads `data-props` on mount and applies patches on update.
|
||||
3. **Svelte 5 component** receives props via `$props()` and re-renders reactively whenever the server sends updates.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Full Svelte 5 support** — `$props()`, `$state()`, `$derived()`, runes syntax, snippets
|
||||
- **Server-side rendering (SSR)** — Optional first-paint HTML via NodeJS (production) or ViteJS (development)
|
||||
- **Efficient props diffing** — Three-tier system: change tracking → JSON Patch → ID-based list diffing
|
||||
- **Phoenix Streams** — Native support for `stream()` with efficient patch operations
|
||||
- **Composables** — `useLiveSvelte`, `useLiveEvent`, `useLiveConnection`, `useLiveNavigation`, `useLiveForm`, `useLiveUpload`, `useEventReply`
|
||||
- **TypeScript** — Full type support across Elixir and JavaScript boundaries
|
||||
- **Igniter installer** — One-command setup with `mix igniter.install live_svelte`
|
||||
- **Vite** — Development HMR and optimized production builds
|
||||
|
||||
## When to Use LiveSvelte
|
||||
|
||||
LiveSvelte is a good fit when you want:
|
||||
|
||||
- Rich, interactive UI components with real-time server state
|
||||
- Svelte 5's reactive primitives and component model alongside LiveView's server logic
|
||||
- Gradual adoption — mix `<.svelte>` components with regular HEEX templates
|
||||
|
||||
Plain LiveView may be sufficient when:
|
||||
|
||||
- Your UI interactions map naturally to LiveView events without needing local component state
|
||||
- You don't need Svelte-specific features like `$derived()` or Svelte snippets
|
||||
|
||||
## Compatibility
|
||||
|
||||
| Dependency | Version |
|
||||
|------------------|----------------------|
|
||||
| LiveSvelte | 0.17.4 |
|
||||
| Svelte | 5.x |
|
||||
| Phoenix | 1.7+ (1.8+ for Igniter installer) |
|
||||
| Phoenix LiveView | 1.0+ |
|
||||
| Elixir | 1.17+ |
|
||||
| Node.js | 19+ (for SSR) |
|
||||
163
guides/ssr.md
Normal file
163
guides/ssr.md
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
# Server-Side Rendering
|
||||
|
||||
LiveSvelte supports server-side rendering (SSR) of Svelte components, which provides meaningful HTML on the first paint before the JavaScript bundle loads.
|
||||
|
||||
## How SSR Works
|
||||
|
||||
When SSR is active, the initial (dead) render calls Node.js to execute Svelte's `render()` function server-side. The result is embedded directly in the HTML response:
|
||||
|
||||
1. LiveSvelte calls `SSR.render(component_name, props, slots)`
|
||||
2. `render()` returns `%{"head" => "<style>...</style>", "html" => "<div>...</div>", "css" => %{"code" => "..."}}`
|
||||
3. The `head` and CSS styles are included in the page; `html` is placed inside the `data-svelte-target` div
|
||||
4. When JavaScript loads, the `SvelteHook` hydrates the existing DOM instead of mounting fresh
|
||||
|
||||
## SSR Modes
|
||||
|
||||
LiveSvelte has two SSR modules for different environments:
|
||||
|
||||
### NodeJS Mode (Production)
|
||||
|
||||
Uses [`elixir-nodejs`](https://github.com/revelrylabs/elixir-nodejs) to run a pool of Node.js workers that execute the SSR bundle.
|
||||
|
||||
```elixir
|
||||
# config/prod.exs
|
||||
config :live_svelte,
|
||||
ssr_module: LiveSvelte.SSR.NodeJS,
|
||||
ssr: true
|
||||
```
|
||||
|
||||
The SSR bundle is built by:
|
||||
```bash
|
||||
mix assets.js # runs: npx vite build --config vite.ssr.config.js
|
||||
```
|
||||
|
||||
This produces `priv/svelte/server.js`, which the NodeJS supervisor loads on application start.
|
||||
|
||||
### ViteJS Mode (Development)
|
||||
|
||||
Forwards SSR requests to the Vite dev server over HTTP. This provides instant HMR without rebuilding the SSR bundle on every change.
|
||||
|
||||
```elixir
|
||||
# config/dev.exs
|
||||
config :live_svelte,
|
||||
ssr_module: LiveSvelte.SSR.ViteJS,
|
||||
vite_host: "http://localhost:5173"
|
||||
```
|
||||
|
||||
> #### ViteJS Mode Requires Vite Dev Server {: .warning}
|
||||
>
|
||||
> `LiveSvelte.SSR.ViteJS` only works when `mix phx.server` is running alongside the Vite dev server started by Phoenix's watchers. If the Vite server is not running, SSR will silently fall back to client-only rendering.
|
||||
|
||||
## Configuration
|
||||
|
||||
Enable/disable SSR globally:
|
||||
|
||||
```elixir
|
||||
# config/config.exs
|
||||
config :live_svelte, ssr: true # enabled (default)
|
||||
config :live_svelte, ssr: false # disabled
|
||||
```
|
||||
|
||||
Select SSR module:
|
||||
|
||||
```elixir
|
||||
config :live_svelte, ssr_module: LiveSvelte.SSR.NodeJS # production (default)
|
||||
config :live_svelte, ssr_module: LiveSvelte.SSR.ViteJS # development
|
||||
```
|
||||
|
||||
## Per-Component SSR Opt-Out
|
||||
|
||||
Disable SSR for a specific component:
|
||||
|
||||
```heex
|
||||
<.svelte name="HeavyChart" props={%{data: @data}} socket={@socket} ssr={false} />
|
||||
```
|
||||
|
||||
Components with `ssr={false}` render a loading slot or nothing on the first paint, then mount client-side normally.
|
||||
|
||||
## HMR in Development
|
||||
|
||||
When running with `LiveSvelte.SSR.ViteJS`, changes to Svelte files trigger automatic hot module replacement. The `SvelteHook` re-mounts affected components without a full page reload.
|
||||
|
||||
Add the `LiveSvelte.Reload` module to your layouts to enable this:
|
||||
|
||||
```elixir
|
||||
# config/dev.exs — added by the Igniter installer
|
||||
config :live_svelte,
|
||||
vite_host: "http://localhost:5173"
|
||||
```
|
||||
|
||||
Use `vite_assets/0` in your layout to include Vite's HMR client:
|
||||
|
||||
```heex
|
||||
<!-- In your root layout (dev only) -->
|
||||
<%= if Application.get_env(:live_svelte, :ssr_module) == LiveSvelte.SSR.ViteJS do %>
|
||||
<LiveSvelte.Reload.vite_assets path="/assets/js/app.js" />
|
||||
<% end %>
|
||||
```
|
||||
|
||||
## Loading Slot
|
||||
|
||||
Show content while a component is loading (only when `ssr={false}`):
|
||||
|
||||
```heex
|
||||
<.svelte name="SlowChart" props={%{data: @data}} socket={@socket} ssr={false}>
|
||||
<:loading>
|
||||
<div class="spinner">Loading chart...</div>
|
||||
</:loading>
|
||||
</.svelte>
|
||||
```
|
||||
|
||||
> #### Loading Slot + SSR Incompatible {: .warning}
|
||||
>
|
||||
> The `:loading` slot is mutually exclusive with SSR. Using both together produces a compile warning. If SSR is active, the loading slot is ignored.
|
||||
|
||||
## Telemetry
|
||||
|
||||
LiveSvelte emits telemetry events for SSR operations:
|
||||
|
||||
| Event | When |
|
||||
|-------|------|
|
||||
| `[:live_svelte, :ssr, :start]` | SSR render begins |
|
||||
| `[:live_svelte, :ssr, :stop]` | SSR render completes; includes `duration_microseconds` measurement |
|
||||
| `[:live_svelte, :ssr, :exception]` | SSR render throws an exception |
|
||||
|
||||
Attach handlers for observability:
|
||||
|
||||
```elixir
|
||||
:telemetry.attach(
|
||||
"live-svelte-ssr",
|
||||
[:live_svelte, :ssr, :stop],
|
||||
fn _event, measurements, _metadata, _config ->
|
||||
Logger.debug("SSR render took #{measurements.duration_microseconds}µs")
|
||||
end,
|
||||
nil
|
||||
)
|
||||
```
|
||||
|
||||
## Testing with SSR
|
||||
|
||||
SSR is disabled in the test environment by default. To write tests that verify SSR output, enable it per test suite:
|
||||
|
||||
```elixir
|
||||
defmodule MyAppWeb.SsrTest do
|
||||
use MyAppWeb.ConnCase, async: false # must be async: false
|
||||
|
||||
setup do
|
||||
Application.put_env(:live_svelte, :ssr, true)
|
||||
on_exit(fn -> Application.put_env(:live_svelte, :ssr, false) end)
|
||||
:ok
|
||||
end
|
||||
|
||||
test "renders SSR HTML", %{conn: conn} do
|
||||
html = conn |> get("/counter") |> html_response(200)
|
||||
assert html =~ "data-ssr=\"true\""
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Use `get/2` + `html_response/2` for initial HTML checks — `visit/2` from PhoenixTest connects the socket and transitions past the dead render.
|
||||
|
||||
## Production Deployment
|
||||
|
||||
See [Deployment](deployment.md) for complete Node.js setup instructions for production.
|
||||
153
guides/streams.md
Normal file
153
guides/streams.md
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
# Phoenix Streams
|
||||
|
||||
LiveSvelte has native support for [Phoenix Streams](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#stream/4), enabling efficient DOM list management without holding full lists in memory on the server.
|
||||
|
||||
## Basic Streams
|
||||
|
||||
**LiveView:**
|
||||
|
||||
```elixir
|
||||
defmodule MyAppWeb.ItemsLive do
|
||||
use MyAppWeb, :live_view
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, stream(socket, :items, MyApp.list_items())}
|
||||
end
|
||||
|
||||
def handle_event("delete", %{"id" => id}, socket) do
|
||||
item = MyApp.get_item!(id)
|
||||
MyApp.delete_item!(item)
|
||||
{:noreply, stream_delete(socket, :items, item)}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.svelte name="ItemList" props={%{items: @streams.items}} socket={@socket} />
|
||||
"""
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Svelte Component:**
|
||||
|
||||
```svelte
|
||||
<!-- assets/svelte/ItemList.svelte -->
|
||||
<script>
|
||||
let { items } = $props()
|
||||
</script>
|
||||
|
||||
{#each items as item (item.__dom_id)}
|
||||
<div id={item.__dom_id}>
|
||||
<p>{item.name}</p>
|
||||
</div>
|
||||
{/each}
|
||||
```
|
||||
|
||||
> #### Use `__dom_id` as the key {: .info}
|
||||
>
|
||||
> Always use `item.__dom_id` as the `{#each}` key. LiveSvelte uses this to track item identity for efficient updates.
|
||||
|
||||
## Stream Operations
|
||||
|
||||
All Phoenix stream operations work automatically:
|
||||
|
||||
```elixir
|
||||
# Insert at the end (default)
|
||||
socket |> stream_insert(socket, :items, new_item)
|
||||
|
||||
# Insert at the beginning
|
||||
socket |> stream_insert(socket, :items, new_item, at: 0)
|
||||
|
||||
# Delete by item (must have :id field)
|
||||
socket |> stream_delete(socket, :items, item)
|
||||
|
||||
# Reset the entire stream
|
||||
socket |> stream(socket, :items, new_items, reset: true)
|
||||
```
|
||||
|
||||
## Efficient Stream Patches
|
||||
|
||||
LiveSvelte sends stream changes as compact JSON Patch operations via `data-streams-diff`, rather than re-sending the full list on every change. This makes stream updates extremely efficient — inserting a single item sends a single operation regardless of list size.
|
||||
|
||||
The patch operations used:
|
||||
|
||||
| Operation | Description |
|
||||
|-----------|-------------|
|
||||
| `upsert` | Insert or update an item at a specific position |
|
||||
| `remove` | Delete an item by `__dom_id` |
|
||||
| `replace` | Reset the entire list |
|
||||
| `limit` | Trim the list to the given max size |
|
||||
|
||||
These are applied client-side by the `SvelteHook` before updating the Svelte component's `items` prop.
|
||||
|
||||
## Accessing Stream Data in Components
|
||||
|
||||
Streams are passed as arrays to Svelte components. Each item has all its original fields plus `__dom_id`:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { messages } = $props()
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
{#each messages as message (message.__dom_id)}
|
||||
<li id={message.__dom_id}>
|
||||
<strong>{message.user}</strong>: {message.text}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
```
|
||||
|
||||
## Multiple Streams
|
||||
|
||||
Pass multiple streams to a single component:
|
||||
|
||||
```elixir
|
||||
def mount(_, _, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> stream(:messages, [])
|
||||
|> stream(:users, [])}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.svelte
|
||||
name="Chat"
|
||||
props={%{messages: @streams.messages, users: @streams.users}}
|
||||
socket={@socket}
|
||||
/>
|
||||
"""
|
||||
end
|
||||
```
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { messages, users } = $props()
|
||||
</script>
|
||||
|
||||
<!-- Both streams update independently and efficiently -->
|
||||
```
|
||||
|
||||
## Encoding Stream Items
|
||||
|
||||
Stream items go through `LiveSvelte.Encoder` before being sent to the client. For custom structs, add `@derive`:
|
||||
|
||||
```elixir
|
||||
defmodule MyApp.Message do
|
||||
@derive {LiveSvelte.Encoder, only: [:id, :user, :text, :inserted_at]}
|
||||
defstruct [:id, :user, :text, :inserted_at]
|
||||
end
|
||||
```
|
||||
|
||||
The `@derive` restriction is enforced — fields not in `only:` are excluded even after `__dom_id` is added.
|
||||
|
||||
## ID-Based Diffing
|
||||
|
||||
For arrays where items have an `:id` field, LiveSvelte uses ID-based list diffing (Tier 3 of the props diffing system). This means:
|
||||
|
||||
- Inserting at position 0 sends a single `upsert` op, not N `replace` ops
|
||||
- Reordering sends minimal operations
|
||||
- List updates stay efficient regardless of list size
|
||||
|
||||
Items must have an `:id` field for ID-based diffing to activate. The `__dom_id` set by Phoenix Streams already guarantees this.
|
||||
249
guides/testing.md
Normal file
249
guides/testing.md
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
# Testing
|
||||
|
||||
The LiveSvelte example project has two complementary test layers: fast server-side tests via PhoenixTest, and full-stack browser tests via Wallaby.
|
||||
|
||||
## Build Before Testing
|
||||
|
||||
> #### Critical: Always Build Before Tests {: .warning}
|
||||
>
|
||||
> After any changes to Svelte components or JS files, always run:
|
||||
>
|
||||
> ```bash
|
||||
> cd example_project
|
||||
> mix assets.js && mix compile
|
||||
> ```
|
||||
>
|
||||
> `mix assets.js` runs Vite builds (client + SSR). `mix compile` copies the updated SSR bundle into `_build/`. Forgetting this step is the most common cause of "my JS changes have no effect" test failures.
|
||||
|
||||
## PhoenixTest (Server-Side, Fast)
|
||||
|
||||
PhoenixTest tests validate server-side behavior without a browser. They are fast, reliable, and do not require chromedriver.
|
||||
|
||||
```bash
|
||||
cd example_project
|
||||
mix test --only phoenix_test
|
||||
```
|
||||
|
||||
Tag test modules with `@moduletag :phoenix_test`:
|
||||
|
||||
```elixir
|
||||
defmodule MyAppWeb.CounterTest do
|
||||
use MyAppWeb.ConnCase, async: true
|
||||
|
||||
@moduletag :phoenix_test
|
||||
|
||||
import PhoenixTest
|
||||
|
||||
test "increments counter", %{conn: conn} do
|
||||
conn
|
||||
|> visit("/counter")
|
||||
|> assert_has("h1", text: "Counter")
|
||||
|> assert_has("[data-props*='\"count\":0']")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### What PhoenixTest Can Verify
|
||||
|
||||
- LiveView renders correct HTML (headings, labels, lists)
|
||||
- `data-props` contains the expected JSON for Svelte components
|
||||
- LiveView events update assigns and re-render correctly
|
||||
- Server-side rendered content
|
||||
|
||||
### What PhoenixTest Cannot Verify
|
||||
|
||||
- Whether Svelte components use the props they receive
|
||||
- Client-side rendering (Svelte output)
|
||||
- Interactions inside Svelte-rendered elements (SSR is off in tests by default)
|
||||
|
||||
### Workaround for Svelte-Rendered Elements
|
||||
|
||||
Use `unwrap/2` to access the LiveView test process and trigger events directly:
|
||||
|
||||
```elixir
|
||||
session
|
||||
|> unwrap(fn view ->
|
||||
Phoenix.LiveViewTest.render_click(view, "increment")
|
||||
end)
|
||||
|> assert_has("[data-props*='\"count\":1']")
|
||||
```
|
||||
|
||||
## Wallaby E2E (Browser-Based, Full Stack)
|
||||
|
||||
Wallaby tests use chromedriver to run a real browser. They validate the full pipeline: LiveView → SvelteHook → Svelte component.
|
||||
|
||||
```bash
|
||||
cd example_project
|
||||
mix test --only e2e
|
||||
```
|
||||
|
||||
Tag test modules with `@moduletag :e2e`:
|
||||
|
||||
```elixir
|
||||
defmodule MyAppWeb.CounterE2ETest do
|
||||
use MyAppWeb.FeatureCase, async: false
|
||||
|
||||
@moduletag :e2e
|
||||
|
||||
test "Svelte counter increments", %{session: session} do
|
||||
session
|
||||
|> visit("/counter")
|
||||
|> assert_text("Count: 0")
|
||||
|> click(button("Increment"))
|
||||
|> assert_text("Count: 1")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### What Wallaby Can Verify
|
||||
|
||||
- Svelte components render the correct data from server props
|
||||
- Client-side interactions (buttons rendered by Svelte, not LiveView)
|
||||
- Full data flow from server through to Svelte re-renders
|
||||
- HMR and dynamic updates
|
||||
|
||||
### Requirements
|
||||
|
||||
Wallaby requires chromedriver installed and available in `PATH`:
|
||||
|
||||
```bash
|
||||
# Check if chromedriver is available
|
||||
chromedriver --version
|
||||
|
||||
# On macOS with Homebrew:
|
||||
brew install chromedriver
|
||||
|
||||
# On Ubuntu/Debian:
|
||||
sudo apt-get install chromium-driver
|
||||
```
|
||||
|
||||
## Running Both Layers
|
||||
|
||||
```bash
|
||||
# Server-side only (fast, no browser needed)
|
||||
mix assets.js && mix test --only phoenix_test
|
||||
|
||||
# Browser E2E only
|
||||
mix assets.js && mix test --only e2e
|
||||
|
||||
# Everything
|
||||
mix assets.js && mix test
|
||||
```
|
||||
|
||||
## `LiveSvelte.Test` — Component Introspection
|
||||
|
||||
`LiveSvelte.Test` provides helper functions to inspect Svelte component props in server-side tests:
|
||||
|
||||
```elixir
|
||||
import LiveSvelte.Test
|
||||
|
||||
# In a PhoenixTest or ConnCase test:
|
||||
{:ok, view, html} = live(conn, "/counter")
|
||||
component = get_svelte(html, name: "Counter")
|
||||
|
||||
assert component.name == "Counter"
|
||||
assert component.props["count"] == 0
|
||||
```
|
||||
|
||||
### `get_svelte/1` and `get_svelte/2`
|
||||
|
||||
```elixir
|
||||
# Get first Svelte component in the HTML
|
||||
component = get_svelte(html)
|
||||
|
||||
# Get component by name
|
||||
component = get_svelte(html, name: "Counter")
|
||||
|
||||
# Get component by DOM id
|
||||
component = get_svelte(html, id: "Counter-1")
|
||||
|
||||
# Get from a LiveView directly
|
||||
{:ok, view, _html} = live(conn, "/counter")
|
||||
component = get_svelte(view, name: "Counter")
|
||||
```
|
||||
|
||||
The returned map has:
|
||||
- `name` — component name string
|
||||
- `id` — DOM id of the component wrapper
|
||||
- `props` — decoded props map (string keys)
|
||||
- `slots` — map of slot name → HTML string
|
||||
- `ssr` — boolean, whether SSR was used
|
||||
|
||||
### Example: Asserting Props after Events
|
||||
|
||||
```elixir
|
||||
test "props update after event", %{conn: conn} do
|
||||
{:ok, view, html} = live(conn, "/counter")
|
||||
|
||||
# Initial props
|
||||
assert get_svelte(html, name: "Counter").props["count"] == 0
|
||||
|
||||
# Trigger event
|
||||
html = render_click(view, "increment")
|
||||
|
||||
# Updated props
|
||||
assert get_svelte(html, name: "Counter").props["count"] == 1
|
||||
end
|
||||
```
|
||||
|
||||
## Vitest (JavaScript Unit Tests)
|
||||
|
||||
JavaScript composables and utilities have unit tests using Vitest:
|
||||
|
||||
```bash
|
||||
cd example_project/assets
|
||||
npm test # Run tests once
|
||||
npm run test:watch # Watch mode
|
||||
```
|
||||
|
||||
Test files are colocated with source files (`*.test.ts`).
|
||||
|
||||
## Tagging Convention
|
||||
|
||||
Use `@moduletag` (not `@tag`) for consistent filtering:
|
||||
|
||||
```elixir
|
||||
# ✅ Correct — applies to ALL tests in the module
|
||||
@moduletag :phoenix_test
|
||||
|
||||
# ❌ Wrong — only applies to the NEXT test
|
||||
@tag :phoenix_test
|
||||
```
|
||||
|
||||
## Test File Layout
|
||||
|
||||
```
|
||||
example_project/test/example_web/
|
||||
├── phoenix_test/ # PhoenixTest (server-side)
|
||||
│ ├── hello_world_test.exs
|
||||
│ ├── live_struct_test.exs
|
||||
│ └── ...
|
||||
└── live/ # Wallaby E2E (browser)
|
||||
├── live_struct_test.exs
|
||||
└── ...
|
||||
```
|
||||
|
||||
## SSR Testing
|
||||
|
||||
SSR is off in tests by default. To test SSR output:
|
||||
|
||||
```elixir
|
||||
defmodule MyAppWeb.SsrTest do
|
||||
use MyAppWeb.ConnCase, async: false # SSR state is global — must use async: false
|
||||
|
||||
setup do
|
||||
Application.put_env(:live_svelte, :ssr, true)
|
||||
on_exit(fn -> Application.put_env(:live_svelte, :ssr, false) end)
|
||||
:ok
|
||||
end
|
||||
|
||||
test "renders SSR HTML on first request", %{conn: conn} do
|
||||
# Use get/html_response for dead render — NOT visit/2
|
||||
html = conn |> get("/counter") |> html_response(200)
|
||||
assert html =~ ~s(data-ssr="true")
|
||||
assert html =~ "<div" # SSR produced some HTML
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
> Use `get/2` + `html_response/2` for SSR checks. `visit/2` from PhoenixTest connects the LiveView socket and transitions past the initial dead render.
|
||||
218
guides/troubleshooting.md
Normal file
218
guides/troubleshooting.md
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
# Troubleshooting
|
||||
|
||||
Common issues encountered when using LiveSvelte, and how to resolve them.
|
||||
|
||||
## "My JS Changes Have No Effect"
|
||||
|
||||
**Symptom:** You modified a Svelte component or JavaScript file, but the browser still shows old behavior.
|
||||
|
||||
**Cause:** Vite hasn't rebuilt the assets, so the browser is loading stale bundles.
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
cd example_project
|
||||
mix assets.js && mix compile
|
||||
```
|
||||
|
||||
`mix assets.js` runs Vite builds (client + SSR). `mix compile` copies the updated SSR bundle into `_build/`. Both steps are required after any change to `assets/`.
|
||||
|
||||
## SSR Renders Stale HTML
|
||||
|
||||
**Symptom:** Server-side rendered HTML shows old component output even after updating Svelte files.
|
||||
|
||||
**Cause:** `_build/test/lib/example/priv/svelte/server.js` is a **copy** (not a symlink) of `priv/svelte/server.js`. It is updated by `mix compile`, not by `mix assets.js` alone.
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
mix assets.js && mix compile
|
||||
```
|
||||
|
||||
If you're seeing stale SSR in tests, ensure the `on_exit` cleanup properly resets SSR state.
|
||||
|
||||
## Svelte CSS Overwrites Tailwind's `app.css`
|
||||
|
||||
**Symptom:** After building, `priv/static/assets/app.css` contains only Svelte component styles, and Tailwind styles are missing.
|
||||
|
||||
**Cause:** The Svelte Vite plugin is extracting component CSS to a file, which overwrites the Tailwind output.
|
||||
|
||||
**Fix:** Ensure your `vite.config.mjs` passes `css: "injected"` to the Svelte plugin:
|
||||
|
||||
```js
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
svelte({
|
||||
compilerOptions: {
|
||||
css: "injected" // ← This is required
|
||||
}
|
||||
}),
|
||||
// ...
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
This injects Svelte component CSS directly into the JS bundle instead of extracting it to a separate file.
|
||||
|
||||
## Component Not Found / `virtual:live-svelte-components` Resolution Fails
|
||||
|
||||
**Symptom:** Browser console shows `Error: Failed to resolve module 'virtual:live-svelte-components'` or a specific component name is not found.
|
||||
|
||||
**Cause:** The `liveSveltePlugin` is missing from one or both Vite configs.
|
||||
|
||||
**Fix:** Ensure `liveSveltePlugin()` is in **both** `vite.config.mjs` (client build) and `vite.ssr.config.js` (SSR build):
|
||||
|
||||
```js
|
||||
// assets/vite.config.mjs
|
||||
import { liveSveltePlugin } from "live_svelte/vitePlugin"
|
||||
plugins: [svelte(), liveSveltePlugin()]
|
||||
|
||||
// assets/vite.ssr.config.js
|
||||
import { liveSveltePlugin } from "live_svelte/vitePlugin"
|
||||
plugins: [svelte(), liveSveltePlugin()]
|
||||
```
|
||||
|
||||
Also verify that your Svelte files are in `assets/svelte/` and have the `.svelte` extension.
|
||||
|
||||
## Wallaby E2E Tests Fail
|
||||
|
||||
**Symptom:** Wallaby tests fail with a browser connection error or chromedriver not found.
|
||||
|
||||
**Cause:** chromedriver is not installed or not in `PATH`.
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# Check installation
|
||||
chromedriver --version
|
||||
|
||||
# Install on macOS
|
||||
brew install chromedriver
|
||||
|
||||
# Install on Ubuntu/Debian
|
||||
sudo apt-get install chromium-driver
|
||||
|
||||
# Or use a specific version with webdriver-manager
|
||||
npm install -g webdriver-manager
|
||||
webdriver-manager update
|
||||
```
|
||||
|
||||
Also ensure `mix assets.js` has been run before E2E tests — the browser needs the built JS to function.
|
||||
|
||||
## `mix live_svelte.install` Says "Task Not Found"
|
||||
|
||||
**Symptom:** Running `mix live_svelte.install` prints "The task 'live_svelte.install' could not be found".
|
||||
|
||||
**Cause:** The `:igniter` dependency is not in your project's deps, or `mix deps.get` hasn't been run.
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# Ensure igniter is installed
|
||||
mix archive.install hex igniter_new
|
||||
|
||||
# Then install using igniter:
|
||||
mix igniter.install live_svelte
|
||||
```
|
||||
|
||||
If you added `{:live_svelte, ...}` to `mix.exs` and ran `mix deps.get` manually, also run:
|
||||
```bash
|
||||
mix deps.compile
|
||||
```
|
||||
|
||||
## `import LiveSvelte` Missing from html_helpers
|
||||
|
||||
**Symptom:** Using `<.svelte>` in a LiveView template produces `function component svelte/1 is undefined`.
|
||||
|
||||
**Cause:** `import LiveSvelte` was not added to the web module's `html_helpers`.
|
||||
|
||||
**Fix:** Add it manually in `lib/my_app_web.ex`:
|
||||
|
||||
```elixir
|
||||
defp html_helpers do
|
||||
quote do
|
||||
# ... existing imports ...
|
||||
import LiveSvelte
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
> Note: use `import LiveSvelte`, not `use LiveSvelte`.
|
||||
|
||||
## Props Not Reactive After Navigation
|
||||
|
||||
**Symptom:** After navigating within the same LiveView, Svelte component props stop updating.
|
||||
|
||||
**Cause:** If `phx-update="ignore"` is missing or incorrectly overridden on the component wrapper, LiveView will re-patch Svelte's DOM on updates, breaking reactivity.
|
||||
|
||||
**Fix:** Do not add `phx-update` attributes to the `<.svelte>` component call — LiveSvelte sets it automatically on the wrapper div. If you are nesting the component inside a container with `phx-update`, ensure that container is set correctly:
|
||||
|
||||
```heex
|
||||
<!-- ✅ Correct — let LiveSvelte manage its own wrapper -->
|
||||
<.svelte name="Counter" props={%{count: @count}} socket={@socket} />
|
||||
|
||||
<!-- ❌ Wrong — wrapping in a plain div that LiveView re-renders -->
|
||||
<div>
|
||||
<.svelte name="Counter" props={%{count: @count}} socket={@socket} />
|
||||
</div>
|
||||
```
|
||||
|
||||
For containers that wrap multiple components, use `phx-update="ignore"` on the outer container if it should not be re-rendered, or ensure each component has a stable `id`.
|
||||
|
||||
## NodeJS.Supervisor Not Starting
|
||||
|
||||
**Symptom:** Application fails to start with `NodeJS.Supervisor` error, or SSR silently fails in production.
|
||||
|
||||
**Cause:** `ssr_module` is not set to `LiveSvelte.SSR.NodeJS` in production config, or the SSR bundle (`priv/svelte/server.js`) is missing.
|
||||
|
||||
**Fix:**
|
||||
1. Ensure `config/prod.exs` has:
|
||||
```elixir
|
||||
config :live_svelte, ssr_module: LiveSvelte.SSR.NodeJS
|
||||
```
|
||||
|
||||
2. Ensure the SSR bundle was built:
|
||||
```bash
|
||||
MIX_ENV=prod mix assets.js && MIX_ENV=prod mix compile
|
||||
```
|
||||
|
||||
3. Check that `NodeJS.Supervisor` is in `application.ex`:
|
||||
```elixir
|
||||
{NodeJS.Supervisor, [path: LiveSvelte.SSR.NodeJS.server_path(), pool_size: 4]}
|
||||
```
|
||||
|
||||
## Svelte 4 Syntax Errors
|
||||
|
||||
**Symptom:** Component fails with unexpected syntax errors like `export let` or `<script context="module">`.
|
||||
|
||||
**Cause:** The component uses Svelte 4 syntax which is not supported in LiveSvelte's Svelte 5 setup.
|
||||
|
||||
**Fix:** Migrate to Svelte 5 runes:
|
||||
|
||||
| Svelte 4 | Svelte 5 |
|
||||
|----------|----------|
|
||||
| `export let count` | `let { count } = $props()` |
|
||||
| `let x = 0` (reactive) | `let x = $state(0)` |
|
||||
| `$: doubled = x * 2` | `let doubled = $derived(x * 2)` |
|
||||
| `import { onMount } from 'svelte'` | same (unchanged) |
|
||||
|
||||
## SSR Not Working in Development
|
||||
|
||||
**Symptom:** Components render without SSR HTML even with `ssr: true` in config.
|
||||
|
||||
**Cause:** `vite_host` is not reachable or Vite dev server isn't running.
|
||||
|
||||
**Fix:** Ensure `mix phx.server` starts the Vite watcher. Check `config/dev.exs`:
|
||||
|
||||
```elixir
|
||||
config :live_svelte,
|
||||
ssr_module: LiveSvelte.SSR.ViteJS,
|
||||
vite_host: "http://localhost:5173"
|
||||
```
|
||||
|
||||
And verify your `config/dev.exs` Phoenix watcher runs `vite`:
|
||||
|
||||
```elixir
|
||||
config :my_app, MyAppWeb.Endpoint,
|
||||
watchers: [
|
||||
node: ["node_modules/.bin/vite", cd: Path.expand("../assets", __DIR__)]
|
||||
]
|
||||
```
|
||||
224
guides/uploads.md
Normal file
224
guides/uploads.md
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
# File Uploads
|
||||
|
||||
LiveSvelte provides `useLiveUpload` for integrating Phoenix LiveView's file upload system with Svelte components.
|
||||
|
||||
## Quick Example
|
||||
|
||||
**LiveView:**
|
||||
|
||||
```elixir
|
||||
defmodule MyAppWeb.UploadLive do
|
||||
use MyAppWeb, :live_view
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:uploaded_files, [])
|
||||
|> allow_upload(:avatar, accept: ~w(.jpg .png), max_entries: 1)}
|
||||
end
|
||||
|
||||
def handle_event("validate", _params, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("submit", _params, socket) do
|
||||
uploaded_files =
|
||||
consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
|
||||
dest = Path.join("priv/static/uploads", Path.basename(path))
|
||||
File.cp!(path, dest)
|
||||
{:ok, "/uploads/#{Path.basename(dest)}"}
|
||||
end)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> update(:uploaded_files, &(&1 ++ uploaded_files))
|
||||
|> put_flash(:info, "Uploaded successfully!")}
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.svelte
|
||||
name="AvatarUpload"
|
||||
props={%{uploads: @uploads}}
|
||||
socket={@socket}
|
||||
/>
|
||||
"""
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Svelte Component:**
|
||||
|
||||
```svelte
|
||||
<!-- assets/svelte/AvatarUpload.svelte -->
|
||||
<script>
|
||||
import { useLiveUpload } from "live_svelte"
|
||||
|
||||
let { uploads } = $props()
|
||||
|
||||
const {
|
||||
showFilePicker,
|
||||
entries,
|
||||
submit,
|
||||
cancel,
|
||||
clear,
|
||||
sync
|
||||
} = useLiveUpload(uploads.avatar, { changeEvent: "validate", submitEvent: "submit" })
|
||||
|
||||
// Keep the composable in sync when the server pushes updated upload config
|
||||
$effect(() => sync(uploads.avatar))
|
||||
</script>
|
||||
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={showFilePicker}
|
||||
onkeydown={(e) => e.key === "Enter" && showFilePicker()}
|
||||
>
|
||||
Click to select a file (or drag and drop)
|
||||
</div>
|
||||
|
||||
{#each $entries as entry (entry.ref)}
|
||||
<div>
|
||||
<p>{entry.client_name}</p>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<progress value={entry.progress} max="100">{entry.progress}%</progress>
|
||||
|
||||
<!-- Validation errors -->
|
||||
{#each entry.errors as error}
|
||||
<p class="error">{error}</p>
|
||||
{/each}
|
||||
|
||||
<button type="button" onclick={() => cancel(entry.ref)}>Remove</button>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<button onclick={submit} disabled={$entries.length === 0}>Upload</button>
|
||||
```
|
||||
|
||||
## The `useLiveUpload` Composable
|
||||
|
||||
```ts
|
||||
import { useLiveUpload } from "live_svelte"
|
||||
|
||||
const { showFilePicker, entries, submit, cancel, clear, sync } = useLiveUpload(
|
||||
uploads.avatar,
|
||||
{ changeEvent: "validate", submitEvent: "submit" }
|
||||
)
|
||||
|
||||
// Sync updated config from server on every render
|
||||
$effect(() => sync(uploads.avatar))
|
||||
```
|
||||
|
||||
The first argument is the **upload config object** for a specific upload field (e.g., `uploads.avatar`). Pass it directly — not as a getter function.
|
||||
|
||||
Call `sync(uploads.avatar)` in a `$effect` to keep the composable up-to-date whenever the server sends an updated config.
|
||||
|
||||
> `useLiveUpload` creates a hidden `<form>` and `<input type="file">` internally and appends them to the LiveView element. You do not need to add a form in your Svelte template.
|
||||
|
||||
### Options
|
||||
|
||||
```ts
|
||||
interface UploadOptions {
|
||||
changeEvent?: string // Server event for phx-change (validation). Optional.
|
||||
submitEvent: string // Server event for phx-submit. REQUIRED.
|
||||
}
|
||||
```
|
||||
|
||||
## Return Values
|
||||
|
||||
| Value | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `showFilePicker()` | `() => void` | Opens the native file picker dialog |
|
||||
| `addFiles(files)` | `(files: File[] \| DataTransfer) => void` | Enqueue files programmatically (for drag-drop) |
|
||||
| `entries` | `Readable<UploadEntry[]>` | Reactive store of current upload entries. Use `$entries` in templates. |
|
||||
| `progress` | `Readable<number>` | Overall upload progress 0–100 averaged across all entries |
|
||||
| `valid` | `Readable<boolean>` | `true` when the upload config has no top-level errors |
|
||||
| `submit()` | `() => void` | Dispatch a form submit event to trigger Phoenix upload |
|
||||
| `cancel(ref?)` | `(ref?: string) => void` | Cancel entry by ref string, or all entries when called with no arg |
|
||||
| `clear()` | `() => void` | Reset the hidden input to clear the file queue |
|
||||
| `sync(config)` | `(config: UploadConfig) => void` | Merge updated config from server. Call in `$effect`. |
|
||||
|
||||
## Upload Entry Fields
|
||||
|
||||
Each entry in `entries` has:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `ref` | `string` | Unique entry identifier |
|
||||
| `client_name` | `string` | Original filename |
|
||||
| `client_size` | `number` | File size in bytes |
|
||||
| `client_type` | `string` | MIME type |
|
||||
| `progress` | `number` | Upload progress (0–100) |
|
||||
| `errors` | `string[]` | Validation error messages |
|
||||
| `valid` | `boolean` | Whether entry passes validation |
|
||||
| `done` | `boolean` | Whether upload is complete |
|
||||
| `preflighted` | `boolean` | Whether Phoenix has acknowledged (preflighted) this entry |
|
||||
|
||||
## Drag and Drop
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { useLiveUpload } from "live_svelte"
|
||||
|
||||
let { uploads } = $props()
|
||||
const { entries, cancel, sync } = useLiveUpload(uploads.avatar, { submitEvent: "submit" })
|
||||
$effect(() => sync(uploads.avatar))
|
||||
let dragOver = $state(false)
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={dragOver ? "drag-over" : ""}
|
||||
ondragover={(e) => { e.preventDefault(); dragOver = true }}
|
||||
ondragleave={() => { dragOver = false }}
|
||||
ondrop={(e) => {
|
||||
e.preventDefault()
|
||||
dragOver = false
|
||||
// Phoenix LiveView handles the drop via phx-drop-target
|
||||
}}
|
||||
phx-drop-target={uploads.avatar?.ref}
|
||||
>
|
||||
Drop files here
|
||||
</div>
|
||||
```
|
||||
|
||||
## Multiple Files
|
||||
|
||||
Configure `max_entries` on the LiveView side:
|
||||
|
||||
```elixir
|
||||
allow_upload(:photos, accept: ~w(.jpg .png .gif), max_entries: 5)
|
||||
```
|
||||
|
||||
The `entries` array in the component will reflect all selected files.
|
||||
|
||||
## Validation
|
||||
|
||||
File validation is configured with `allow_upload/3` options:
|
||||
|
||||
```elixir
|
||||
allow_upload(:avatar,
|
||||
accept: ~w(.jpg .png .webp),
|
||||
max_entries: 1,
|
||||
max_file_size: 10_000_000 # 10 MB
|
||||
)
|
||||
```
|
||||
|
||||
Validation errors appear in `entry.errors` as human-readable strings.
|
||||
|
||||
## Progress Tracking
|
||||
|
||||
Upload progress is automatically tracked per entry via `entry.progress` (0–100):
|
||||
|
||||
```svelte
|
||||
{#each $entries as entry (entry.ref)}
|
||||
<div class="upload-item">
|
||||
<span>{entry.client_name}</span>
|
||||
<div class="progress-bar" style="width: {entry.progress}%"></div>
|
||||
{#if entry.done}
|
||||
<span>✓ Complete</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
```
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
defmodule LiveSvelte.LiveJson do
|
||||
use Phoenix.Component
|
||||
|
||||
attr(
|
||||
:live_json_props,
|
||||
:map,
|
||||
default: %{},
|
||||
doc: "LiveJSON props to pass to the svelte component"
|
||||
)
|
||||
|
||||
attr(
|
||||
:svelte_id,
|
||||
:string,
|
||||
required: true,
|
||||
doc: "Stable DOM id from the parent svelte component"
|
||||
)
|
||||
|
||||
slot(:inner_block)
|
||||
|
||||
def live_json(assigns) do
|
||||
~H"""
|
||||
<%= if @live_json_props != %{} do %>
|
||||
<div id={"lj-#{@svelte_id}"} phx-hook="LiveJSON" />
|
||||
<% end %>
|
||||
<%= render_slot(@inner_block) %>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
@ -7,7 +7,6 @@ defmodule LiveSvelte do
|
|||
|
||||
use Phoenix.Component
|
||||
import Phoenix.HTML
|
||||
import LiveSvelte.LiveJson
|
||||
|
||||
alias Phoenix.LiveView
|
||||
alias Phoenix.LiveView.LiveStream
|
||||
|
|
@ -55,11 +54,6 @@ defmodule LiveSvelte do
|
|||
default: nil,
|
||||
doc: "LiveView socket, only needed when ssr: true"
|
||||
|
||||
attr :live_json_props, :map,
|
||||
default: %{},
|
||||
doc: "LiveJson props to pass to the Svelte component",
|
||||
examples: [%{my_big_data_set: %{some_data: 1}}]
|
||||
|
||||
attr :diff, :boolean,
|
||||
default: true,
|
||||
doc:
|
||||
|
|
@ -120,11 +114,7 @@ defmodule LiveSvelte do
|
|||
ssr_code =
|
||||
if init and dead and ssr_active and assigns.ssr do
|
||||
try do
|
||||
props =
|
||||
Map.merge(
|
||||
Map.get(assigns, :props, %{}),
|
||||
Map.get(assigns, :live_json_props, %{})
|
||||
)
|
||||
props = Map.get(assigns, :props, %{})
|
||||
|
||||
SSR.render(assigns.name, props, slots)
|
||||
rescue
|
||||
|
|
@ -146,36 +136,31 @@ defmodule LiveSvelte do
|
|||
|> assign(:streams_diff, streams_diff)
|
||||
|
||||
~H"""
|
||||
<.live_json live_json_props={@live_json_props} svelte_id={@svelte_id}>
|
||||
<script>
|
||||
<script>
|
||||
<%= raw(@ssr_render["head"]) %>
|
||||
</script>
|
||||
<div
|
||||
id={@svelte_id}
|
||||
data-name={@name}
|
||||
data-props={json(@props_to_send)}
|
||||
data-props-diff={json(@props_diff)}
|
||||
data-streams-diff={json(@streams_diff)}
|
||||
data-use-diff={to_string(@use_diff)}
|
||||
data-ssr={@ssr_render != nil}
|
||||
data-slots={@slots |> Slots.base_encode_64() |> json}
|
||||
phx-hook="SvelteHook"
|
||||
phx-update="ignore"
|
||||
class={@class}
|
||||
>
|
||||
<div id={"#{@svelte_id}-target"} data-svelte-target>
|
||||
<%= raw(@ssr_render["head"]) %>
|
||||
</script>
|
||||
<div
|
||||
id={@svelte_id}
|
||||
data-name={@name}
|
||||
data-props={json(@props_to_send)}
|
||||
data-props-diff={json(@props_diff)}
|
||||
data-streams-diff={json(@streams_diff)}
|
||||
data-use-diff={to_string(@use_diff)}
|
||||
data-ssr={@ssr_render != nil}
|
||||
data-live-json={
|
||||
if @init, do: json(@live_json_props), else: @live_json_props |> Map.keys() |> json()
|
||||
}
|
||||
data-slots={@slots |> Slots.base_encode_64() |> json}
|
||||
phx-hook="SvelteHook"
|
||||
phx-update="ignore"
|
||||
class={@class}
|
||||
>
|
||||
<div id={"#{@svelte_id}-target"} data-svelte-target>
|
||||
<%= raw(@ssr_render["head"]) %>
|
||||
<style>
|
||||
<%= raw(@ssr_render["css"]["code"]) %>
|
||||
</style>
|
||||
<%= raw(@ssr_render["html"]) %>
|
||||
<%= render_slot(@loading) %>
|
||||
</div>
|
||||
<style>
|
||||
<%= raw(@ssr_render["css"]["code"]) %>
|
||||
</style>
|
||||
<%= raw(@ssr_render["html"]) %>
|
||||
<%= render_slot(@loading) %>
|
||||
</div>
|
||||
</.live_json>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
|
|
|
|||
38
mix.exs
38
mix.exs
|
|
@ -26,12 +26,44 @@ defmodule LiveSvelte.MixProject do
|
|||
source_ref: "v#{@version}",
|
||||
source_url: @repo_url,
|
||||
homepage_url: @repo_url,
|
||||
main: "LiveSvelte",
|
||||
main: "readme",
|
||||
logo: "logo_3.png",
|
||||
links: %{
|
||||
"GitHub" => @repo_url,
|
||||
"Sponsor" => "https://github.com/sponsors/woutdp"
|
||||
}
|
||||
},
|
||||
extras: [
|
||||
"README.md": [title: "LiveSvelte"],
|
||||
|
||||
# Getting Started
|
||||
"guides/installation.md": [title: "Installation"],
|
||||
"guides/basic_usage.md": [title: "Basic Usage"],
|
||||
|
||||
# Core Usage
|
||||
"guides/forms.md": [title: "Forms and Validation"],
|
||||
"guides/uploads.md": [title: "File Uploads"],
|
||||
"guides/streams.md": [title: "Phoenix Streams"],
|
||||
"guides/ssr.md": [title: "Server-Side Rendering"],
|
||||
"guides/configuration.md": [title: "Configuration"],
|
||||
|
||||
# Reference
|
||||
"guides/api_reference.md": [title: "API Reference"],
|
||||
|
||||
# Advanced Topics
|
||||
"guides/introduction.md": [title: "Introduction"],
|
||||
"guides/testing.md": [title: "Testing"],
|
||||
"guides/deployment.md": [title: "Deployment"],
|
||||
|
||||
# Help & Troubleshooting
|
||||
"guides/troubleshooting.md": [title: "Troubleshooting"]
|
||||
],
|
||||
groups_for_extras: [
|
||||
"Getting Started": ~r/guides\/(installation|basic_usage)/,
|
||||
"Core Usage": ~r/guides\/(forms|uploads|streams|ssr|configuration)/,
|
||||
Reference: ~r/guides\/api_reference/,
|
||||
"Advanced Topics": ~r/guides\/(introduction|testing|deployment)/,
|
||||
"Help & Troubleshooting": ~r/guides\/troubleshooting/
|
||||
]
|
||||
]
|
||||
]
|
||||
end
|
||||
|
|
@ -45,7 +77,7 @@ defmodule LiveSvelte.MixProject do
|
|||
GitHub: @repo_url
|
||||
},
|
||||
files:
|
||||
~w(assets/copy/tsconfig.json assets/js lib mix.exs package.json .formatter.exs LICENSE.md README.md CHANGELOG.md)
|
||||
~w(assets/copy/tsconfig.json assets/js guides lib mix.exs package.json .formatter.exs LICENSE.md README.md CHANGELOG.md)
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
|||
177
package-lock.json
generated
177
package-lock.json
generated
|
|
@ -9,38 +9,31 @@
|
|||
"version": "0.17.4",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"prettier": "3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.7",
|
||||
"svelte": "^5.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
"svelte": "^5.53.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/remapping": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
||||
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
|
|
@ -53,16 +46,6 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
|
|
@ -71,9 +54,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -81,17 +64,34 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/acorn-typescript": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz",
|
||||
"integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -102,20 +102,10 @@
|
|||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-typescript": {
|
||||
"version": "1.4.13",
|
||||
"resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz",
|
||||
"integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"acorn": ">=8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aria-query": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
|
|
@ -132,32 +122,48 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz",
|
||||
"integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esm-env": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.1.4.tgz",
|
||||
"integrity": "sha512-oO82nKPHKkzIj/hbtuDYy/JHqBHFlMIW36SDiPCVsj87ntDLcWN+sJ1erdVryd4NxODacFTsdrIE3b7IamqbOg==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.2.2.tgz",
|
||||
"integrity": "sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz",
|
||||
"integrity": "sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||
"@types/estree": "^1.0.1"
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
}
|
||||
},
|
||||
"node_modules/is-reference": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
|
||||
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
"@types/estree": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-character": {
|
||||
|
|
@ -178,9 +184,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -195,9 +201,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-svelte": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.7.tgz",
|
||||
"integrity": "sha512-/Dswx/ea0lV34If1eDcG3nulQ63YNr5KPDfMsjbdtpSWOxKKJ7nAc2qlVuYwEvCr4raIuredNoR7K4JCkmTGaQ==",
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz",
|
||||
"integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
|
|
@ -206,23 +212,26 @@
|
|||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.1.13",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.1.13.tgz",
|
||||
"integrity": "sha512-xVNk8yLsZNfkyqWzVg8+nfU9ewiSjVW0S4qyTxfKa6Y7P5ZBhA+LDsh2cHWIXJQMltikQAk6W3sqGdQZSH58PA==",
|
||||
"version": "5.53.7",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.7.tgz",
|
||||
"integrity": "sha512-uxck1KI7JWtlfP3H6HOWi/94soAl23jsGJkBzN2BAWcQng0+lTrRNhxActFqORgnO9BHVd1hKJhG+ljRuIUWfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
"@types/estree": "^1.0.5",
|
||||
"@types/trusted-types": "^2.0.7",
|
||||
"acorn": "^8.12.1",
|
||||
"acorn-typescript": "^1.4.13",
|
||||
"aria-query": "^5.3.1",
|
||||
"aria-query": "5.3.1",
|
||||
"axobject-query": "^4.1.0",
|
||||
"esm-env": "^1.0.0",
|
||||
"esrap": "^1.2.2",
|
||||
"is-reference": "^3.0.2",
|
||||
"clsx": "^2.1.1",
|
||||
"devalue": "^5.6.3",
|
||||
"esm-env": "^1.2.1",
|
||||
"esrap": "^2.2.2",
|
||||
"is-reference": "^3.0.3",
|
||||
"locate-character": "^3.0.0",
|
||||
"magic-string": "^0.30.11",
|
||||
"zimmerframe": "^1.1.2"
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@
|
|||
"format": "npx prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.7",
|
||||
"svelte": "^5.1.13"
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-svelte": "^3.5.1",
|
||||
"svelte": "^5.53.7"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ defmodule LiveSvelte.AutoIdTest do
|
|||
id: Keyword.get(opts, :id),
|
||||
key: Keyword.get(opts, :key),
|
||||
props: Keyword.get(opts, :props, %{}),
|
||||
live_json_props: %{},
|
||||
ssr: false,
|
||||
class: nil,
|
||||
loading: [],
|
||||
|
|
@ -298,19 +297,9 @@ defmodule LiveSvelte.AutoIdTest do
|
|||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests — live_json and phx-update (unchanged from before)
|
||||
# Tests — phx-update
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
describe "live_json id derivation" do
|
||||
test "live_json div id is prefixed with lj- and uses the svelte id" do
|
||||
html = render_html(base_assigns("Counter", props: %{x: 1}))
|
||||
|
||||
# When live_json_props is empty, no lj- div is rendered.
|
||||
# Verify the component id is still correct.
|
||||
assert extract_id(html) == "Counter"
|
||||
end
|
||||
end
|
||||
|
||||
describe "phx-update attribute" do
|
||||
test "outer hook container has phx-update=ignore to protect Svelte DOM" do
|
||||
html = render_html(base_assigns("Counter"))
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ defmodule LiveSvelte.JSONLibraryTest do
|
|||
socket: nil,
|
||||
name: "Test",
|
||||
props: data,
|
||||
live_json_props: %{},
|
||||
ssr: false,
|
||||
class: nil,
|
||||
loading: [],
|
||||
|
|
@ -58,7 +57,6 @@ defmodule LiveSvelte.JSONLibraryTest do
|
|||
socket: nil,
|
||||
name: "Test",
|
||||
props: data,
|
||||
live_json_props: %{},
|
||||
ssr: false,
|
||||
class: nil,
|
||||
loading: [],
|
||||
|
|
@ -87,7 +85,6 @@ defmodule LiveSvelte.JSONLibraryTest do
|
|||
socket: nil,
|
||||
name: "Legacy",
|
||||
props: data,
|
||||
live_json_props: %{},
|
||||
ssr: false,
|
||||
class: nil,
|
||||
loading: [],
|
||||
|
|
@ -120,7 +117,6 @@ defmodule LiveSvelte.JSONLibraryTest do
|
|||
socket: nil,
|
||||
name: "StructTest",
|
||||
props: data,
|
||||
live_json_props: %{},
|
||||
ssr: false,
|
||||
class: nil,
|
||||
loading: [],
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ defmodule LiveSvelte.JSONTest do
|
|||
assert decoded["2"] == "b"
|
||||
end
|
||||
|
||||
test "encodes large maps with integer keys (LiveJson scenario)" do
|
||||
test "encodes large maps with integer keys" do
|
||||
data = for i <- 1..100, into: %{}, do: {i, i * 2}
|
||||
result = JSON.encode!(data)
|
||||
decoded = :json.decode(result)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ defmodule LiveSvelte.PropsDiffTest do
|
|||
id: nil,
|
||||
key: nil,
|
||||
props: Keyword.get(opts, :props, %{}),
|
||||
live_json_props: %{},
|
||||
ssr: false,
|
||||
class: nil,
|
||||
loading: [],
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ defmodule LiveSvelte.StreamsTest do
|
|||
id: nil,
|
||||
key: nil,
|
||||
props: %{},
|
||||
live_json_props: %{},
|
||||
ssr: false,
|
||||
class: nil,
|
||||
loading: [],
|
||||
|
|
|
|||
Loading…
Reference in a new issue