mirror of
https://github.com/woutdp/live_svelte
synced 2026-05-24 09:28:21 +00:00
chore: use phoenix_vite for live svelte
This commit is contained in:
parent
60ddc047ef
commit
35e8870b05
31 changed files with 17617 additions and 357 deletions
65
README.md
65
README.md
|
|
@ -118,7 +118,7 @@ defp deps do
|
|||
end
|
||||
```
|
||||
|
||||
2. Fetch and run the installer (this adds `live_svelte`, configures Vite, app.js, html_helpers, SSR, and more):
|
||||
2. Fetch and run the installer (this adds `live_svelte`, configures [phoenix_vite](https://github.com/LostKobrakai/phoenix_vite), Vite, app.js, html_helpers, SSR, layout with `PhoenixVite.Components.assets`, and more):
|
||||
|
||||
```bash
|
||||
mix deps.get
|
||||
|
|
@ -128,9 +128,8 @@ mix igniter.install live_svelte
|
|||
3. Install npm packages and build assets:
|
||||
|
||||
```bash
|
||||
npm install # from the assets/ directory, or:
|
||||
# cd assets && npm install && cd ..
|
||||
mix assets.js # runs both Vite builds (client + SSR) + Tailwind
|
||||
mix assets.setup # phoenix_vite.npm assets install
|
||||
mix assets.build # Vite client + SSR builds (or run mix setup to do both)
|
||||
```
|
||||
|
||||
4. Start the server:
|
||||
|
|
@ -139,6 +138,8 @@ mix assets.js # runs both Vite builds (client + SSR) + Tailwind
|
|||
mix phx.server
|
||||
```
|
||||
|
||||
With phoenix_vite, the layout uses `PhoenixVite.Components.assets` and the endpoint uses `PhoenixVite.Plug`; no separate Vite terminal is required for dev — phoenix_vite integrates the Vite dev server.
|
||||
|
||||
Visit `http://localhost:4000/svelte_demo` to confirm the demo Svelte component is working.
|
||||
|
||||
Add `--bun` to use Bun instead of npm/npx:
|
||||
|
|
@ -181,26 +182,32 @@ defp html_helpers do
|
|||
end
|
||||
```
|
||||
|
||||
**4.** Add the `assets.js` alias and update `assets.deploy` in `mix.exs`:
|
||||
**4.** Add phoenix_vite and configure mix aliases (or use the Igniter installer which does this). With phoenix_vite:
|
||||
|
||||
```elixir
|
||||
defp aliases do
|
||||
[
|
||||
# ...
|
||||
"assets.js": [
|
||||
"cmd --cd assets npx vite build",
|
||||
"cmd --cd assets npx vite build --config vite.ssr.config.js",
|
||||
"tailwind default"
|
||||
],
|
||||
"assets.deploy": [
|
||||
"cmd --cd assets npx vite build --mode production",
|
||||
"cmd --cd assets npx vite build --config vite.ssr.config.js --mode production",
|
||||
"phx.digest"
|
||||
]
|
||||
# mix.exs deps
|
||||
{:phoenix_vite, "~> 0.4"}
|
||||
|
||||
# config/config.exs
|
||||
config :phoenix_vite, PhoenixVite.Npm,
|
||||
assets: [args: [], cd: __DIR__],
|
||||
vite: [
|
||||
args: ~w(exec -- vite),
|
||||
cd: Path.expand("../assets", __DIR__),
|
||||
env: %{"MIX_BUILD_PATH" => Mix.Project.build_path()}
|
||||
]
|
||||
end
|
||||
|
||||
# mix.exs aliases
|
||||
"assets.setup": ["phoenix_vite.npm assets install"],
|
||||
"assets.build": [
|
||||
"phoenix_vite.npm vite build --manifest --emptyOutDir true",
|
||||
"phoenix_vite.npm vite build --ssrManifest --emptyOutDir false --ssr js/server.js --outDir ../priv/svelte"
|
||||
],
|
||||
"assets.deploy": ["assets.build", "phx.digest"]
|
||||
```
|
||||
|
||||
Use `PhoenixVite.Components.assets` in your root layout and `import PhoenixVite.Plug` + `plug :favicon, dev_server: {PhoenixVite.Components, :has_vite_watcher?, [__MODULE__]}` in the endpoint. Without phoenix_vite, use `LiveSvelte.Reload.vite_assets` in the layout and run Vite manually.
|
||||
|
||||
**5.** Update `assets/vite.config.mjs` to add the Svelte and LiveSvelte plugins:
|
||||
|
||||
```js
|
||||
|
|
@ -215,25 +222,7 @@ plugins: [
|
|||
]
|
||||
```
|
||||
|
||||
**6.** Create `assets/vite.ssr.config.js`:
|
||||
|
||||
```js
|
||||
import { defineConfig } from "vite"
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import liveSveltePlugin from "live_svelte/vitePlugin"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte(), liveSveltePlugin({ entrypoint: "./js/server.js" })],
|
||||
ssr: { noExternal: true },
|
||||
build: {
|
||||
ssr: "./js/server.js",
|
||||
outDir: "../priv/svelte",
|
||||
rollupOptions: {
|
||||
output: { entryFileNames: "server.js", format: "es" }
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
**6.** Add `ssr: { noExternal: process.env.NODE_ENV === "production" ? true : undefined }` to the main `vite.config.mjs` so the same config is used for both client and SSR builds. The SSR build is run via `phoenix_vite.npm vite build --ssr js/server.js --outDir ../priv/svelte` (see aliases above). No separate `vite.ssr.config.js` is required when using phoenix_vite.
|
||||
|
||||
**7.** Create `assets/js/server.js`:
|
||||
|
||||
|
|
|
|||
59
example_project/assets/js/app.js
Normal file
59
example_project/assets/js/app.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Client-side entry point for both Vite dev server (HMR) and production builds.
|
||||
import "phoenix_html"
|
||||
import {Socket} from "phoenix"
|
||||
import {LiveSocket} from "phoenix_live_view"
|
||||
import topbar from "topbar"
|
||||
import {getHooks} from "live_svelte"
|
||||
import Components from "virtual:live-svelte-components"
|
||||
|
||||
function formatPayload(str) {
|
||||
if (!str || str === "—") return "—"
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(str), null, 2)
|
||||
} catch {
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
const PropsDiffPayloadDisplay = {
|
||||
mounted() {
|
||||
this.updateDisplays()
|
||||
const root = this.el
|
||||
const diffOnEl = root.querySelector("[data-name='PropsDiffDemo'][data-use-diff='true']")
|
||||
const diffOffEl = root.querySelector("[data-name='PropsDiffDemo'][data-use-diff='false']")
|
||||
const observer = new MutationObserver(() => this.updateDisplays())
|
||||
if (diffOnEl) observer.observe(diffOnEl, {attributes: true, attributeFilter: ["data-props"]})
|
||||
if (diffOffEl) observer.observe(diffOffEl, {attributes: true, attributeFilter: ["data-props"]})
|
||||
this._observer = observer
|
||||
},
|
||||
updated() {
|
||||
this.updateDisplays()
|
||||
},
|
||||
destroyed() {
|
||||
this._observer?.disconnect()
|
||||
},
|
||||
updateDisplays() {
|
||||
const root = this.el
|
||||
const diffOnEl = root.querySelector("[data-name='PropsDiffDemo'][data-use-diff='true']")
|
||||
const diffOffEl = root.querySelector("[data-name='PropsDiffDemo'][data-use-diff='false']")
|
||||
const preOn = root.querySelector("#payload-display-diff-on")
|
||||
const preOff = root.querySelector("#payload-display-diff-off")
|
||||
if (preOn) preOn.textContent = formatPayload(diffOnEl?.getAttribute("data-props") ?? "—")
|
||||
if (preOff) preOff.textContent = formatPayload(diffOffEl?.getAttribute("data-props") ?? "—")
|
||||
},
|
||||
}
|
||||
|
||||
const Hooks = {
|
||||
...getHooks(Components),
|
||||
PropsDiffPayloadDisplay,
|
||||
}
|
||||
|
||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}})
|
||||
|
||||
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
|
||||
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
|
||||
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
|
||||
|
||||
liveSocket.connect()
|
||||
window.liveSocket = liveSocket
|
||||
5
example_project/assets/js/server.js
Normal file
5
example_project/assets/js/server.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// SSR entry point for LiveSvelte (used by --ssr js/server.js).
|
||||
import { getRender } from "live_svelte"
|
||||
import Components from "virtual:live-svelte-components"
|
||||
|
||||
export const render = getRender(Components)
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@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.53.7",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"live_svelte": "file:../..",
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html",
|
||||
"phoenix_live_view": "file:../deps/phoenix_live_view"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
<script lang="ts">
|
||||
import Child from "./Child.svelte"
|
||||
// import Child from "./Child.svelte"
|
||||
let {children}: { children?: import("svelte").Snippet } = $props()
|
||||
</script>
|
||||
|
||||
<div class="card bg-base-100 shadow-md border border-base-300/50 overflow-hidden md:w-md p-5">
|
||||
<h3 class="badge badge-outline badge-sm font-medium text-base-content/70">Parent Svelte component</h3>
|
||||
<div class="card-body">
|
||||
<Child onClick={() => alert("Event triggered in child and handled in parent")} />
|
||||
<!-- <Child onClick={() => alert("Event triggered in child and handled in parent")} /> -->
|
||||
<div class="mt-5">
|
||||
<h5 class="badge badge-outline badge-sm font-medium text-base-content/70">Slot Item</h5>
|
||||
{@render children?.()}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<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
|
||||
Click me !!!
|
||||
</button>
|
||||
<span data-testid="click-count" class="font-mono">{clicks}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,43 +1,41 @@
|
|||
import { defineConfig } from "vite"
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import liveSveltePlugin from "live_svelte/vitePlugin"
|
||||
import tailwindcss from "@tailwindcss/vite"
|
||||
import { fileURLToPath } from "url"
|
||||
import path from "path"
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
svelte({ compilerOptions: { css: "injected" } }),
|
||||
liveSveltePlugin({ entrypoint: "./js/server.vite.js" }),
|
||||
],
|
||||
server: {
|
||||
host: "127.0.0.1",
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
cors: { origin: "http://localhost:4000" },
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ["live_svelte", "phoenix", "phoenix_html", "phoenix_live_view"],
|
||||
},
|
||||
ssr: { noExternal: process.env.NODE_ENV === "production" ? true : undefined },
|
||||
build: {
|
||||
manifest: false,
|
||||
ssrManifest: false,
|
||||
rollupOptions: {
|
||||
input: ["js/app.js", "css/app.css"],
|
||||
},
|
||||
outDir: "../priv/static",
|
||||
emptyOutDir: true,
|
||||
},
|
||||
resolve: {
|
||||
// Explicit alias so Vite always resolves live_svelte to library TypeScript
|
||||
// source, regardless of package.json export condition availability.
|
||||
alias: {
|
||||
"@": ".",
|
||||
live_svelte: path.resolve(__dirname, "../../assets/js/live_svelte/index.ts"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
commonjsOptions: { include: [/vendor\//, /node_modules\//] },
|
||||
target: "es2020",
|
||||
outDir: "../priv/static/assets",
|
||||
emptyOutDir: true,
|
||||
sourcemap: false,
|
||||
manifest: false,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
app: path.resolve(__dirname, "./js/app.vite.js"),
|
||||
},
|
||||
output: {
|
||||
entryFileNames: "[name].js",
|
||||
chunkFileNames: "[name].js",
|
||||
assetFileNames: "[name][extname]",
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: "localhost",
|
||||
port: 5173,
|
||||
},
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
svelte({ compilerOptions: { css: "injected" } }),
|
||||
liveSveltePlugin({ entrypoint: "./js/server.js" }),
|
||||
],
|
||||
})
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
import { defineConfig } from "vite"
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import liveSveltePlugin from "live_svelte/vitePlugin"
|
||||
import { fileURLToPath } from "url"
|
||||
import path from "path"
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
svelte(),
|
||||
liveSveltePlugin({ entrypoint: "./js/server.vite.js" }),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
live_svelte: path.resolve(__dirname, "../../assets/js/live_svelte/index.ts"),
|
||||
},
|
||||
},
|
||||
ssr: {
|
||||
// Bundle all dependencies into the output file so it works as a standalone
|
||||
// Node.js module (mirrors the old esbuild `bundle: true` behavior).
|
||||
noExternal: true,
|
||||
},
|
||||
build: {
|
||||
ssr: "./js/server.vite.js",
|
||||
outDir: "../priv/svelte",
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: "server.js",
|
||||
format: "es",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -7,6 +7,14 @@
|
|||
# General application configuration
|
||||
import Config
|
||||
|
||||
config :phoenix_vite, PhoenixVite.Npm,
|
||||
assets: [args: [], cd: __DIR__],
|
||||
vite: [
|
||||
args: ~w(exec -- vite),
|
||||
cd: Path.expand("../assets", __DIR__),
|
||||
env: %{"MIX_BUILD_PATH" => Mix.Project.build_path()}
|
||||
]
|
||||
|
||||
config :example,
|
||||
ecto_repos: [Example.Repo]
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ defmodule ExampleWeb do
|
|||
those modules here.
|
||||
"""
|
||||
|
||||
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
|
||||
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt js css)
|
||||
|
||||
def router do
|
||||
quote do
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@
|
|||
<.live_title suffix=" · Phoenix Framework">
|
||||
{assigns[:page_title] || "Example"}
|
||||
</.live_title>
|
||||
<LiveSvelte.Reload.vite_assets assets={["/js/app.vite.js"]}>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||
</script>
|
||||
</LiveSvelte.Reload.vite_assets>
|
||||
<PhoenixVite.Components.assets
|
||||
names={["js/app.js", "css/app.css"]}
|
||||
manifest={{:example, "priv/static/.vite/manifest.json"}}
|
||||
dev_server={PhoenixVite.Components.has_vite_watcher?(ExampleWeb.Endpoint)}
|
||||
to_url={fn p -> static_url(@conn, p) end}
|
||||
/>
|
||||
</head>
|
||||
<body class="bg-base-200 antialiased" data-theme="light">
|
||||
{@inner_content}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
defmodule ExampleWeb.Endpoint do
|
||||
use Phoenix.Endpoint, otp_app: :example
|
||||
import PhoenixVite.Plug
|
||||
|
||||
# The session will be stored in the cookie and signed,
|
||||
# this means its contents can be read but not tampered with.
|
||||
|
|
@ -13,6 +14,8 @@ defmodule ExampleWeb.Endpoint do
|
|||
|
||||
socket "/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]
|
||||
|
||||
plug :favicon, dev_server: {PhoenixVite.Components, :has_vite_watcher?, [__MODULE__]}
|
||||
|
||||
# In test, prevent caching of assets so E2E always loads the latest app.js after mix assets.js
|
||||
if Mix.env() == :test do
|
||||
plug :no_cache_assets
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ defmodule ExampleWeb.Router do
|
|||
live("/live-id-list-diff", LiveIdListDiff)
|
||||
live("/live-slots-simple", LiveSlotsSimple)
|
||||
live("/live-slots-dynamic", LiveSlotsDynamic)
|
||||
live("/live-slots-nested", LiveSlotsNested)
|
||||
# live("/live-slots-nested", LiveSlotsNested)
|
||||
live("/live-client-side-loading", LiveClientSideLoading)
|
||||
# Ecto Examples
|
||||
live("/live-notes-otp", LiveNotesOtp)
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ defmodule Example.MixProject do
|
|||
{:wallaby, "~> 0.30", runtime: false, only: :test},
|
||||
{:phoenix_test, "~> 0.9", only: :test},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:phoenix_vite, "~> 0.4"},
|
||||
{:tailwind, "~> 0.3", runtime: Mix.env() == :dev}
|
||||
]
|
||||
end
|
||||
|
|
@ -64,24 +65,20 @@ defmodule Example.MixProject do
|
|||
# See the documentation for `Mix` for more info on aliases.
|
||||
defp aliases do
|
||||
[
|
||||
setup: ["deps.get", "ecto.setup", "cmd --cd assets npm install"],
|
||||
setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
|
||||
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
|
||||
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
||||
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
|
||||
"assets.js": [
|
||||
"cmd --cd assets npx vite build",
|
||||
"cmd --cd assets npx vite build --config vite.ssr.config.js",
|
||||
"tailwind default"
|
||||
"assets.setup": ["phoenix_vite.npm assets install", "tailwind.install --if-missing"],
|
||||
"assets.build": [
|
||||
"phoenix_vite.npm vite build --manifest --emptyOutDir true",
|
||||
"phoenix_vite.npm vite build --ssrManifest --emptyOutDir false --ssr js/server.js --outDir ../priv/svelte"
|
||||
],
|
||||
"test.e2e": ["assets.js", "ecto.create --quiet", "ecto.migrate --quiet", "test"],
|
||||
"assets.setup": ["tailwind.install --if-missing"],
|
||||
"assets.build": ["tailwind default"],
|
||||
"assets.deploy": [
|
||||
"cmd --cd assets npx vite build",
|
||||
"cmd --cd assets npx vite build --config vite.ssr.config.js",
|
||||
"tailwind default --minify",
|
||||
"assets.build",
|
||||
"phx.digest"
|
||||
]
|
||||
],
|
||||
"test.e2e": ["assets.build", "ecto.create --quiet", "ecto.migrate --quiet", "test"]
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
|||
17305
example_project/package-lock.json
generated
Normal file
17305
example_project/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
20
example_project/package.json
Normal file
20
example_project/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"live_svelte": "file:../",
|
||||
"phoenix": "file:./deps/phoenix",
|
||||
"phoenix_html": "file:./deps/phoenix_html",
|
||||
"phoenix_live_view": "file:./deps/phoenix_live_view",
|
||||
"topbar": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/vite": "^4.1.0",
|
||||
"daisyui": "^5.0.0",
|
||||
"phoenix_vite": "file:./deps/phoenix_vite",
|
||||
"svelte": "^5.53.7",
|
||||
"tailwindcss": "^4.1.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^6.3.0"
|
||||
}
|
||||
}
|
||||
17
example_project/priv/static/.vite/manifest.json
Normal file
17
example_project/priv/static/.vite/manifest.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"css/app.css": {
|
||||
"file": "assets/app-DqfLVfXj.css",
|
||||
"src": "css/app.css",
|
||||
"isEntry": true,
|
||||
"name": "app",
|
||||
"names": [
|
||||
"app.css"
|
||||
]
|
||||
},
|
||||
"js/app.js": {
|
||||
"file": "assets/app-BVVDu27N.js",
|
||||
"name": "app",
|
||||
"src": "js/app.js",
|
||||
"isEntry": true
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB |
|
|
@ -1,5 +0,0 @@
|
|||
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
||||
#
|
||||
# To ban all spiders from the entire site uncomment the next two lines:
|
||||
# User-agent: *
|
||||
# Disallow: /
|
||||
Binary file not shown.
|
|
@ -1,5 +0,0 @@
|
|||
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
|
||||
#
|
||||
# To ban all spiders from the entire site uncomment the next two lines:
|
||||
# User-agent: *
|
||||
# Disallow: /
|
||||
Binary file not shown.
|
|
@ -129,7 +129,9 @@ Without `@derive`, passing a struct as a prop will raise an error.
|
|||
|
||||
---
|
||||
|
||||
### `LiveSvelte.Reload` / `vite_assets/0`
|
||||
### `LiveSvelte.Reload` / `vite_assets/1`
|
||||
|
||||
When using the Igniter installer with phoenix_vite, the layout uses `PhoenixVite.Components.assets` instead. Use `LiveSvelte.Reload.vite_assets/1` when not using phoenix_vite (e.g. manual setup).
|
||||
|
||||
HMR helper for development. Includes the Vite dev server client script.
|
||||
|
||||
|
|
|
|||
|
|
@ -11,26 +11,23 @@ Deploying a LiveSvelte application requires Node.js on the server for SSR (serve
|
|||
|
||||
```bash
|
||||
# 1. Build client bundle and SSR bundle
|
||||
mix assets.js
|
||||
mix assets.build
|
||||
|
||||
# 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
|
||||
MIX_ENV=prod mix assets.build && MIX_ENV=prod mix release
|
||||
```
|
||||
|
||||
### What `mix assets.js` Does
|
||||
### What `mix assets.build` Does
|
||||
|
||||
The `assets.js` alias runs (in order):
|
||||
The `assets.build` 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`
|
||||
1. `phoenix_vite.npm vite build --manifest --emptyOutDir true` — client bundle (and CSS when using Tailwind via Vite) to `priv/static/`
|
||||
2. `phoenix_vite.npm vite build --ssrManifest ... --ssr js/server.js --outDir ../priv/svelte` — SSR bundle to `priv/svelte/server.js`
|
||||
|
||||
> #### 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.
|
||||
> The same `assets/vite.config.mjs` is used for both builds; phoenix_vite runs the second command with different CLI flags.
|
||||
|
||||
## NodeJS Supervisor
|
||||
|
||||
|
|
@ -68,15 +65,15 @@ config :live_svelte,
|
|||
## SSR Bundle
|
||||
|
||||
The SSR bundle (`priv/svelte/server.js`) is:
|
||||
- Built from `assets/vite.ssr.config.js`
|
||||
- Built via the same `assets/vite.config.mjs` with `--ssr js/server.js --outDir ../priv/svelte`
|
||||
- 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.
|
||||
After `mix assets.build`, `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.
|
||||
> After `mix assets.build`, run `mix compile` so `_build/` gets the updated SSR bundle. In a CI/CD pipeline, ensure both steps run.
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
|
|
@ -89,7 +86,7 @@ WORKDIR /app
|
|||
COPY assets/ assets/
|
||||
COPY deps/ deps/
|
||||
RUN cd assets && npm install
|
||||
RUN mix assets.js
|
||||
RUN mix assets.build
|
||||
|
||||
FROM elixir:1.17-slim AS release-builder
|
||||
# ... standard Elixir release steps ...
|
||||
|
|
@ -159,5 +156,5 @@ 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`
|
||||
4. Rebuild: `mix assets.build && mix compile`
|
||||
5. Run tests: `mix test`
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ mix igniter.install live_svelte --bun
|
|||
### Step 3: Install JS dependencies and build
|
||||
|
||||
```bash
|
||||
cd assets && npm install && cd ..
|
||||
mix assets.js
|
||||
mix assets.setup # phoenix_vite.npm assets install (or: mix setup)
|
||||
mix assets.build
|
||||
mix phx.server
|
||||
```
|
||||
|
||||
|
|
@ -51,21 +51,12 @@ Visit `/svelte_demo` to verify the installation with the generated demo componen
|
|||
|
||||
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)
|
||||
**`package.json`** (at project root) — the installer moves it from `assets/` and adds:
|
||||
- `live_svelte`, `phoenix_vite: "file:./deps/phoenix_vite"` (dev), and Svelte-related deps
|
||||
|
||||
**`assets/vite.config.mjs`** — adds the Svelte plugin and `liveSveltePlugin`:
|
||||
```js
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import { liveSveltePlugin } from "live_svelte/vitePlugin"
|
||||
**`config/config.exs`** — adds `config :phoenix_vite, PhoenixVite.Npm, ...`
|
||||
|
||||
// ...
|
||||
plugins: [svelte(), liveSveltePlugin()],
|
||||
```
|
||||
|
||||
**`assets/vite.ssr.config.js`** — new file for the SSR bundle (Node.js server rendering)
|
||||
**`assets/vite.config.mjs`** — adds the Svelte plugin, `liveSveltePlugin`, and `ssr: { noExternal: ... }`. A single config is used for both client and SSR builds (no separate `vite.ssr.config.js`); the SSR build is run via `phoenix_vite.npm vite build --ssr js/server.js --outDir ../priv/svelte`.
|
||||
|
||||
**`assets/js/app.js`** — adds hook wiring:
|
||||
```js
|
||||
|
|
@ -104,12 +95,12 @@ config :live_svelte,
|
|||
ssr: true
|
||||
```
|
||||
|
||||
**`mix.exs`** — adds the `assets.js` alias that runs both Vite builds plus Tailwind:
|
||||
**`mix.exs`** — adds phoenix_vite-driven aliases: `assets.setup`, `assets.build` (client + SSR via `phoenix_vite.npm vite build`):
|
||||
```elixir
|
||||
"assets.js": [
|
||||
"cmd npx vite build",
|
||||
"cmd npx vite build --config vite.ssr.config.js",
|
||||
"tailwind default"
|
||||
"assets.setup": ["phoenix_vite.npm assets install", "tailwind.install --if-missing"],
|
||||
"assets.build": [
|
||||
"phoenix_vite.npm vite build --manifest --emptyOutDir true",
|
||||
"phoenix_vite.npm vite build --ssrManifest --emptyOutDir false --ssr js/server.js --outDir ../priv/svelte"
|
||||
]
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ config :live_svelte,
|
|||
|
||||
The SSR bundle is built by:
|
||||
```bash
|
||||
mix assets.js # runs: npx vite build --config vite.ssr.config.js
|
||||
mix assets.build # runs phoenix_vite.npm vite build (client + SSR)
|
||||
```
|
||||
|
||||
This produces `priv/svelte/server.js`, which the NodeJS supervisor loads on application start.
|
||||
|
|
@ -79,7 +79,7 @@ Components with `ssr={false}` render a loading slot or nothing on the first pain
|
|||
|
||||
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:
|
||||
When using phoenix_vite, the layout uses `PhoenixVite.Components.assets` and the Vite dev server is integrated automatically. When not using phoenix_vite, add the `LiveSvelte.Reload` module to your layouts to enable this:
|
||||
|
||||
```elixir
|
||||
# config/dev.exs — added by the Igniter installer
|
||||
|
|
@ -87,13 +87,13 @@ config :live_svelte,
|
|||
vite_host: "http://localhost:5173"
|
||||
```
|
||||
|
||||
Use `vite_assets/0` in your layout to include Vite's HMR client:
|
||||
When not using phoenix_vite, use `LiveSvelte.Reload.vite_assets/1` in your layout to include Vite's HMR client and production fallback:
|
||||
|
||||
```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 %>
|
||||
<LiveSvelte.Reload.vite_assets assets={["/js/app.js"]}>
|
||||
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}></script>
|
||||
</LiveSvelte.Reload.vite_assets>
|
||||
```
|
||||
|
||||
## Loading Slot
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ The LiveSvelte example project has two complementary test layers: fast server-si
|
|||
>
|
||||
> ```bash
|
||||
> cd example_project
|
||||
> mix assets.js && mix compile
|
||||
> mix assets.build && 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.
|
||||
> `mix assets.build` 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)
|
||||
|
||||
|
|
@ -121,13 +121,13 @@ sudo apt-get install chromium-driver
|
|||
|
||||
```bash
|
||||
# Server-side only (fast, no browser needed)
|
||||
mix assets.js && mix test --only phoenix_test
|
||||
mix assets.build && mix test --only phoenix_test
|
||||
|
||||
# Browser E2E only
|
||||
mix assets.js && mix test --only e2e
|
||||
mix assets.build && mix test --only e2e
|
||||
|
||||
# Everything
|
||||
mix assets.js && mix test
|
||||
mix assets.build && mix test
|
||||
```
|
||||
|
||||
## `LiveSvelte.Test` — Component Introspection
|
||||
|
|
|
|||
|
|
@ -11,20 +11,20 @@ Common issues encountered when using LiveSvelte, and how to resolve them.
|
|||
**Fix:**
|
||||
```bash
|
||||
cd example_project
|
||||
mix assets.js && mix compile
|
||||
mix assets.build && 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/`.
|
||||
`mix assets.build` 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.
|
||||
**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.build` alone.
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
mix assets.js && mix compile
|
||||
mix assets.build && mix compile
|
||||
```
|
||||
|
||||
If you're seeing stale SSR in tests, ensure the `on_exit` cleanup properly resets SSR state.
|
||||
|
|
@ -60,16 +60,13 @@ This injects Svelte component CSS directly into the JS bundle instead of extract
|
|||
|
||||
**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):
|
||||
**Fix:** Ensure `liveSveltePlugin()` is in `vite.config.mjs` and that `ssr: { noExternal: ... }` is set. The same config is used for both client and SSR builds (via `phoenix_vite.npm vite build --ssr js/server.js ...`).
|
||||
|
||||
```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()]
|
||||
import liveSveltePlugin from "live_svelte/vitePlugin"
|
||||
plugins: [svelte(), liveSveltePlugin({ entrypoint: "./js/server.js" })],
|
||||
ssr: { noExternal: process.env.NODE_ENV === "production" ? true : undefined },
|
||||
```
|
||||
|
||||
Also verify that your Svelte files are in `assets/svelte/` and have the `.svelte` extension.
|
||||
|
|
@ -96,7 +93,7 @@ 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.
|
||||
Also ensure `mix assets.build` has been run before E2E tests — the browser needs the built JS to function.
|
||||
|
||||
## `mix live_svelte.install` Says "Task Not Found"
|
||||
|
||||
|
|
@ -171,7 +168,7 @@ For containers that wrap multiple components, use `phx-update="ignore"` on the o
|
|||
|
||||
2. Ensure the SSR bundle was built:
|
||||
```bash
|
||||
MIX_ENV=prod mix assets.js && MIX_ENV=prod mix compile
|
||||
MIX_ENV=prod mix assets.build && MIX_ENV=prod mix compile
|
||||
```
|
||||
|
||||
3. Check that `NodeJS.Supervisor` is in `application.ex`:
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
igniter
|
||||
|> Igniter.compose_task("phoenix_vite.install", igniter.args.argv)
|
||||
|> configure_environments(app_name)
|
||||
|> update_phoenix_vite_config()
|
||||
|> add_live_svelte_to_html_helpers(app_name)
|
||||
|> update_javascript_configuration()
|
||||
|> configure_tailwind_for_svelte()
|
||||
|
|
@ -59,7 +60,16 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
|> add_svelte_demo_route()
|
||||
|> update_home_template()
|
||||
|> update_gitignore()
|
||||
|> create_ssr_vite_config()
|
||||
end
|
||||
|
||||
defp update_phoenix_vite_config(igniter) do
|
||||
Config.configure(
|
||||
igniter,
|
||||
"config.exs",
|
||||
:phoenix_vite,
|
||||
[PhoenixVite.Npm, :assets],
|
||||
{:code, Sourceror.parse_string!(~s|[args: [], cd: __DIR__]|)}
|
||||
)
|
||||
end
|
||||
|
||||
# Configure environments (config.exs, dev.exs, prod.exs)
|
||||
|
|
@ -196,8 +206,6 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
end
|
||||
|
||||
# Update vite.config.mjs to add Svelte plugin and liveSveltePlugin.
|
||||
# Note: ssr.noExternal is intentionally NOT added here — it only applies to SSR
|
||||
# builds and is already set in the separate vite.ssr.config.js.
|
||||
defp update_vite_configuration(igniter) do
|
||||
Igniter.update_file(igniter, "assets/vite.config.mjs", fn source ->
|
||||
Rewrite.Source.update(source, :content, fn content ->
|
||||
|
|
@ -205,10 +213,23 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
|> add_svelte_vite_imports()
|
||||
|> update_vite_optimized_deps()
|
||||
|> update_vite_plugins()
|
||||
|> add_ssr_vite_entry()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp add_ssr_vite_entry(content) do
|
||||
if String.contains?(content, "noExternal") do
|
||||
content
|
||||
else
|
||||
String.replace(
|
||||
content,
|
||||
~r/build: \{/s,
|
||||
"ssr: { noExternal: process.env.NODE_ENV === \"production\" ? true : undefined },\n build: {"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp add_svelte_vite_imports(content) do
|
||||
if String.contains?(content, "import { svelte }") do
|
||||
content
|
||||
|
|
@ -245,20 +266,35 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
end
|
||||
end
|
||||
|
||||
# Update package.json with Svelte dependencies
|
||||
# phoenix_vite.install creates assets/package.json; npm runs from assets/
|
||||
# Move package.json to root (like live_vue) and add Svelte + phoenix_vite dependencies.
|
||||
# phoenix_vite.install creates assets/package.json; we move to root and patch.
|
||||
defp update_package_json_for_svelte(igniter) do
|
||||
igniter
|
||||
|> Igniter.update_file("assets/package.json", fn source ->
|
||||
|> Igniter.move_file("assets/package.json", "package.json")
|
||||
|> Igniter.update_file("package.json", fn source ->
|
||||
Rewrite.Source.update(source, :content, fn content ->
|
||||
content
|
||||
|> add_module_type()
|
||||
|> add_svelte_dependency()
|
||||
|> add_svelte_dev_dependencies()
|
||||
|> add_phoenix_vite_dev_dependency()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp add_phoenix_vite_dev_dependency(content) do
|
||||
if String.contains?(content, "\"phoenix_vite\"") do
|
||||
content
|
||||
else
|
||||
String.replace(
|
||||
content,
|
||||
~s("vite":),
|
||||
~s("phoenix_vite": "file:./deps/phoenix_vite",\n "vite":),
|
||||
global: false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp add_svelte_dependency(content) do
|
||||
if String.contains?(content, "\"live_svelte\"") do
|
||||
content
|
||||
|
|
@ -349,67 +385,23 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
end)
|
||||
end
|
||||
|
||||
# Update mix.exs aliases in the consumer app to use Vite
|
||||
# Update mix.exs aliases: replace single phoenix_vite.npm vite build with two-step (client + SSR).
|
||||
defp update_mix_aliases(igniter) do
|
||||
bun? = igniter.args.options[:bun] || false
|
||||
pm = if bun?, do: "bunx", else: "npx"
|
||||
|
||||
Igniter.update_file(igniter, "mix.exs", fn source ->
|
||||
Rewrite.Source.update(source, :content, fn content ->
|
||||
if String.contains?(content, ~s("assets.js":)) do
|
||||
if String.contains?(content, "js/server.js") do
|
||||
content
|
||||
else
|
||||
content
|
||||
|> add_assets_js_alias(pm)
|
||||
|> update_assets_deploy_alias(pm)
|
||||
String.replace(
|
||||
content,
|
||||
~s("phoenix_vite.npm vite build"),
|
||||
~s("phoenix_vite.npm vite build --manifest --emptyOutDir true", "phoenix_vite.npm vite build --ssrManifest --emptyOutDir false --ssr js/server.js --outDir ../priv/svelte")
|
||||
)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp add_assets_js_alias(content, pm) do
|
||||
if String.contains?(content, "\"assets.js\"") do
|
||||
content
|
||||
else
|
||||
String.replace(
|
||||
content,
|
||||
~r/("assets\.setup":)/,
|
||||
~s("assets.js": [\n "assets.build",\n "cmd --cd assets #{pm} vite build --config vite.ssr.config.js"\n ],\n \\1)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp update_assets_deploy_alias(content, pm) do
|
||||
ssr_cmd = ~s("cmd --cd assets #{pm} vite build --config vite.ssr.config.js --mode production")
|
||||
|
||||
cond do
|
||||
# Guard: SSR step already present in assets.deploy (idempotent).
|
||||
# Check for --mode production which is unique to the deploy command;
|
||||
# assets.js also references vite.ssr.config.js so a generic check would
|
||||
# fire immediately after add_assets_js_alias runs, skipping the deploy update.
|
||||
String.contains?(content, "vite.ssr.config.js --mode production") ->
|
||||
content
|
||||
|
||||
# phoenix_vite.install pattern: assets.deploy is a list containing "assets.build".
|
||||
# Use dotall regex so inline and multi-line list formats both match.
|
||||
Regex.match?(~r/"assets\.deploy":\s*\[/s, content) ->
|
||||
String.replace(
|
||||
content,
|
||||
~r/("assets\.deploy":\s*\[)(.*?)(\s*\])/s,
|
||||
"\\1\\2,\n #{ssr_cmd}\\3",
|
||||
global: false
|
||||
)
|
||||
|
||||
# Legacy esbuild pattern — replace with vite production builds
|
||||
true ->
|
||||
String.replace(
|
||||
content,
|
||||
~r/"esbuild default[^"]*"/,
|
||||
~s("cmd --cd assets #{pm} vite build --mode production", #{ssr_cmd})
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Add svelte_demo route to router
|
||||
defp add_svelte_demo_route(igniter) do
|
||||
web_module = Phoenix.web_module(igniter)
|
||||
|
|
@ -467,25 +459,21 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
end)
|
||||
end
|
||||
|
||||
# Add gitignore entries for Svelte build artifacts under a named section.
|
||||
# Add gitignore entries; with package.json at root, node_modules is at project root.
|
||||
defp update_gitignore(igniter) do
|
||||
Igniter.update_file(igniter, ".gitignore", fn source ->
|
||||
Rewrite.Source.update(source, :content, fn content ->
|
||||
if String.contains?(content, "/priv/svelte/") do
|
||||
content
|
||||
else
|
||||
String.trim_trailing(content) <>
|
||||
"\n\n# LiveSvelte build artifacts\n/assets/svelte/_build/\n/priv/svelte/\n"
|
||||
end
|
||||
content
|
||||
|> then(fn c ->
|
||||
if String.contains?(c, "/assets/node_modules"), do: String.replace(c, "/assets/node_modules", "node_modules"), else: c
|
||||
end)
|
||||
|> then(fn c ->
|
||||
if String.contains?(c, "/priv/svelte/"), do: c, else: String.trim_trailing(c) <> "\n\n# LiveSvelte build artifacts\n/assets/svelte/_build/\n/priv/svelte/\n"
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
# Create the SSR Vite config file
|
||||
defp create_ssr_vite_config(igniter) do
|
||||
Igniter.create_new_file(igniter, "assets/vite.ssr.config.js", ssr_vite_config_content())
|
||||
end
|
||||
|
||||
# Content helpers
|
||||
|
||||
defp server_js_content do
|
||||
|
|
@ -496,26 +484,6 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
"""
|
||||
end
|
||||
|
||||
defp ssr_vite_config_content do
|
||||
"""
|
||||
import { defineConfig } from "vite"
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import liveSveltePlugin from "live_svelte/vitePlugin"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte(), liveSveltePlugin({ entrypoint: "./js/server.js" })],
|
||||
ssr: { noExternal: true },
|
||||
build: {
|
||||
ssr: "./js/server.js",
|
||||
outDir: "../priv/svelte",
|
||||
rollupOptions: {
|
||||
output: { entryFileNames: "server.js", format: "es" }
|
||||
}
|
||||
}
|
||||
})
|
||||
"""
|
||||
end
|
||||
|
||||
defp demo_svelte_content do
|
||||
"""
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -53,62 +53,32 @@ defmodule Mix.Tasks.LiveSvelte.InstallTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "H2 regression: update_assets_deploy_alias/2" do
|
||||
test "assets.deploy includes SSR vite build step" do
|
||||
describe "H2: phoenix_vite two-step assets.build" do
|
||||
test "assets.build includes phoenix_vite.npm client and SSR build steps" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "mix.exs")
|
||||
|
||||
assert content =~ "vite.ssr.config.js",
|
||||
"assets.deploy is missing the SSR build step:\n\n#{content}"
|
||||
end
|
||||
assert content =~ "phoenix_vite.npm vite build --manifest --emptyOutDir true",
|
||||
"assets.build missing client step:\n\n#{content}"
|
||||
|
||||
test "assets.deploy SSR step uses npx by default" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "mix.exs")
|
||||
assert content =~ "phoenix_vite.npm vite build --ssrManifest",
|
||||
"assets.build missing SSR step:\n\n#{content}"
|
||||
|
||||
assert content =~
|
||||
~r/npx vite build --config vite\.ssr\.config\.js --mode production/,
|
||||
"Expected npx SSR build in assets.deploy"
|
||||
end
|
||||
assert content =~ "--ssr js/server.js",
|
||||
"assets.build SSR step must use js/server.js entry:\n\n#{content}"
|
||||
|
||||
test "assets.deploy SSR step uses bunx with --bun flag" do
|
||||
result = run_installer(bun: true)
|
||||
content = file_content(result, "mix.exs")
|
||||
|
||||
assert content =~
|
||||
~r/bunx vite build --config vite\.ssr\.config\.js --mode production/,
|
||||
"Expected bunx SSR build in assets.deploy with --bun"
|
||||
assert content =~ "--outDir ../priv/svelte",
|
||||
"assets.build SSR step must output to priv/svelte:\n\n#{content}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "assets.js alias" do
|
||||
test "assets.js alias is added with client and SSR build steps" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "mix.exs")
|
||||
|
||||
assert content =~ ~s("assets.js":),
|
||||
"assets.js alias is missing from mix.exs"
|
||||
|
||||
assert content =~ ~r/"assets\.js".*vite\.ssr\.config\.js/s,
|
||||
"assets.js alias missing SSR step"
|
||||
end
|
||||
end
|
||||
|
||||
describe "M2: vite config — no noExternal in client config" do
|
||||
test "client vite.config.mjs does not contain noExternal" do
|
||||
describe "M2: vite config — ssr noExternal in main config" do
|
||||
test "vite.config.mjs contains ssr noExternal for production" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "assets/vite.config.mjs")
|
||||
|
||||
refute content =~ "noExternal",
|
||||
"ssr.noExternal must NOT appear in client vite.config.mjs (belongs only in vite.ssr.config.js)"
|
||||
end
|
||||
|
||||
test "SSR vite.ssr.config.js correctly contains noExternal" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "assets/vite.ssr.config.js")
|
||||
|
||||
assert content =~ "noExternal: true",
|
||||
"vite.ssr.config.js must have ssr.noExternal: true"
|
||||
assert content =~ "noExternal",
|
||||
"vite.config.mjs must have ssr.noExternal for SSR build"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -133,9 +103,10 @@ defmodule Mix.Tasks.LiveSvelte.InstallTest do
|
|||
end
|
||||
|
||||
describe "config files" do
|
||||
test "config.exs sets ssr: true" do
|
||||
test "config.exs sets phoenix_vite and live_svelte ssr" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "config/config.exs")
|
||||
assert content =~ ~r/:phoenix_vite.*PhoenixVite\.Npm/s
|
||||
assert content =~ ~r/:live_svelte.*ssr.*true/s
|
||||
end
|
||||
|
||||
|
|
@ -152,16 +123,17 @@ defmodule Mix.Tasks.LiveSvelte.InstallTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "package.json" do
|
||||
test "live_svelte dependency added" do
|
||||
describe "package.json (root)" do
|
||||
test "package.json is at project root with live_svelte dependency" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "assets/package.json")
|
||||
content = file_content(result, "package.json")
|
||||
assert content =~ "live_svelte"
|
||||
end
|
||||
|
||||
test "svelte dev dependencies added" do
|
||||
test "phoenix_vite and svelte dev dependencies added" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "assets/package.json")
|
||||
content = file_content(result, "package.json")
|
||||
assert content =~ "phoenix_vite"
|
||||
assert content =~ "@sveltejs/vite-plugin-svelte"
|
||||
assert content =~ ~s("svelte":)
|
||||
end
|
||||
|
|
@ -226,11 +198,6 @@ defmodule Mix.Tasks.LiveSvelte.InstallTest do
|
|||
end
|
||||
|
||||
describe "created files" do
|
||||
test "assets/vite.ssr.config.js is created" do
|
||||
result = run_installer()
|
||||
assert_creates(result, "assets/vite.ssr.config.js")
|
||||
end
|
||||
|
||||
test "assets/js/server.js is created" do
|
||||
result = run_installer()
|
||||
assert_creates(result, "assets/js/server.js")
|
||||
|
|
|
|||
Loading…
Reference in a new issue