mirror of
https://github.com/woutdp/live_svelte
synced 2026-05-24 09:28:21 +00:00
chore: improve igniter installer
This commit is contained in:
parent
3f2684be40
commit
beb462fafb
5 changed files with 564 additions and 102 deletions
220
README.md
220
README.md
|
|
@ -88,62 +88,224 @@ If you don't want SSR, you can disable it by not setting `NodeJS.Supervisor` in
|
|||
|
||||
_If you're updating from an older version, make sure to check the `CHANGELOG.md` for breaking changes._
|
||||
|
||||
1. Add `live_svelte` to your list of dependencies of your Phoenix app in `mix.exs`:
|
||||
LiveSvelte uses [Vite](https://vite.dev/) as its build tool with `@sveltejs/vite-plugin-svelte` for Svelte compilation. The recommended way to install is via the [Igniter](https://github.com/ash-project/igniter) installer, which automates all configuration steps.
|
||||
|
||||
### Option A — Igniter installer (recommended)
|
||||
|
||||
Requires Phoenix 1.8+ and Node.js 19+.
|
||||
|
||||
#### New project
|
||||
|
||||
```bash
|
||||
mix archive.install hex igniter_new
|
||||
mix igniter.new my_app --with phx.new --install live_svelte
|
||||
cd my_app
|
||||
mix setup
|
||||
mix phx.server
|
||||
```
|
||||
|
||||
#### Existing project
|
||||
|
||||
Igniter must be present in the project's deps before the installer can run.
|
||||
|
||||
1. Add igniter to `mix.exs`:
|
||||
|
||||
```elixir
|
||||
defp deps do
|
||||
[
|
||||
{:live_svelte, "~> 0.17.4"}
|
||||
# ... existing deps ...
|
||||
{:igniter, "~> 0.6"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
2. Adjust the `setup` and `assets.deploy` aliases in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
defp aliases do
|
||||
[
|
||||
setup: ["deps.get", "ecto.setup", "cmd --cd assets npm install"],
|
||||
...,
|
||||
"assets.deploy": ["tailwind <app_name> --minify", "cmd --cd assets npx vite build", "phx.digest"]
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Note: `tailwind <app_name> --minify` is only required in the `assets.deploy` alias if you're using Tailwind. If you are not using Tailwind, you can remove it from the list.
|
||||
|
||||
3. Run the following in your terminal
|
||||
2. Fetch and run the installer (this adds `live_svelte`, configures Vite, app.js, html_helpers, SSR, and more):
|
||||
|
||||
```bash
|
||||
mix deps.get
|
||||
mix live_svelte.setup
|
||||
mix igniter.install live_svelte
|
||||
```
|
||||
|
||||
4. Add `import LiveSvelte` in `html_helpers/0` inside `/lib/<app_name>_web.ex` like so:
|
||||
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
|
||||
```
|
||||
|
||||
4. Start the server:
|
||||
|
||||
```bash
|
||||
mix phx.server
|
||||
```
|
||||
|
||||
Visit `http://localhost:4000/svelte_demo` to confirm the demo Svelte component is working.
|
||||
|
||||
Add `--bun` to use Bun instead of npm/npx:
|
||||
|
||||
```bash
|
||||
mix igniter.install live_svelte --bun
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option B — Manual installation
|
||||
|
||||
Use this if you prefer not to use Igniter, or need full control over the configuration.
|
||||
|
||||
**1.** Add dependencies to `mix.exs`:
|
||||
|
||||
```elixir
|
||||
# /lib/<app_name>_web.ex
|
||||
defp deps do
|
||||
[
|
||||
{:live_svelte, "~> 0.17"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
**2.** Fetch deps:
|
||||
|
||||
```bash
|
||||
mix deps.get
|
||||
```
|
||||
|
||||
**3.** Add `import LiveSvelte` in `html_helpers/0` inside `lib/<app_name>_web.ex`:
|
||||
|
||||
```elixir
|
||||
defp html_helpers do
|
||||
quote do
|
||||
|
||||
# ...
|
||||
|
||||
import LiveSvelte # <-- Add this line
|
||||
|
||||
import LiveSvelte
|
||||
# ...
|
||||
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
5. For tailwind support, add `@source "../svelte";` in the `app.css` file
|
||||
**4.** Add the `assets.js` alias and update `assets.deploy` in `mix.exs`:
|
||||
|
||||
6. Finally, remove the `esbuild` configuration from `config/config.exs` and remove the dependency from the `deps` function in your `mix.exs`, and you are done!
|
||||
```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"
|
||||
]
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
### What did we do?
|
||||
**5.** Update `assets/vite.config.mjs` to add the Svelte and LiveSvelte plugins:
|
||||
|
||||
LiveSvelte uses [Vite](https://vite.dev/) as its build tool with `@sveltejs/vite-plugin-svelte` for Svelte compilation. The `mix live_svelte.setup` task sets up the necessary Vite configuration in your `assets/` directory.
|
||||
```js
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import liveSveltePlugin from "live_svelte/vitePlugin"
|
||||
|
||||
// Inside defineConfig plugins array:
|
||||
plugins: [
|
||||
svelte({ compilerOptions: { css: "injected" } }),
|
||||
liveSveltePlugin({ entrypoint: "./js/server.js" }),
|
||||
// ... existing 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" }
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**7.** Create `assets/js/server.js`:
|
||||
|
||||
```js
|
||||
import { getRender } from "live_svelte"
|
||||
import Components from "virtual:live-svelte-components"
|
||||
export const render = getRender(Components)
|
||||
```
|
||||
|
||||
**8.** Update `assets/js/app.js` to wire in the LiveSvelte hooks:
|
||||
|
||||
```js
|
||||
import { getHooks } from "live_svelte"
|
||||
import Components from "virtual:live-svelte-components"
|
||||
|
||||
// Update your LiveSocket hooks:
|
||||
let liveSocket = new LiveSocket("/live", Socket, {
|
||||
hooks: { ...getHooks(Components) },
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
**9.** Update `assets/package.json` to add Svelte dependencies:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"live_svelte": "file:./deps/live_svelte"
|
||||
},
|
||||
"devDependencies": {
|
||||
"svelte": "^5.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**10.** Add SSR configuration to `config/config.exs`, `config/dev.exs`, and `config/prod.exs`:
|
||||
|
||||
```elixir
|
||||
# config/config.exs
|
||||
config :live_svelte, ssr: true
|
||||
|
||||
# config/dev.exs
|
||||
config :live_svelte,
|
||||
ssr_module: LiveSvelte.SSR.ViteJS,
|
||||
vite_host: "http://localhost:5173"
|
||||
|
||||
# config/prod.exs
|
||||
config :live_svelte,
|
||||
ssr_module: LiveSvelte.SSR.NodeJS,
|
||||
ssr: true
|
||||
```
|
||||
|
||||
**11.** Add `NodeJS.Supervisor` to `lib/<app_name>/application.ex`:
|
||||
|
||||
```elixir
|
||||
children = [
|
||||
{NodeJS.Supervisor, [path: LiveSvelte.SSR.NodeJS.server_path(), pool_size: 4]},
|
||||
# ... existing children
|
||||
]
|
||||
```
|
||||
|
||||
**12.** For Tailwind support, add `@source "../svelte";` to `assets/css/app.css`.
|
||||
|
||||
**13.** Install npm packages and build:
|
||||
|
||||
```bash
|
||||
cd assets && npm install && cd ..
|
||||
mix assets.js
|
||||
mix phx.server
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^2.1.0",
|
||||
"esbuild": "^0.24.0",
|
||||
|
|
|
|||
15
example_project/assets/svelte/Nested.svelte
Normal file
15
example_project/assets/svelte/Nested.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
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")} />
|
||||
<div class="mt-5">
|
||||
<h5 class="badge badge-outline badge-sm font-medium text-base-content/70">Slot Item</h5>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -16,6 +16,14 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
|
||||
"""
|
||||
|
||||
# Force-load Igniter from the BEAM path before the with_igniter macro expands.
|
||||
# Code.ensure_loaded?/1 only checks modules already in memory; when Mix compiles
|
||||
# live_svelte as a newly-fetched dep, Igniter's BEAM files are on the path but not
|
||||
# yet loaded into the runtime, causing ensure_loaded? to return false and the else
|
||||
# (plain Mix.Task) branch to compile. ensure_compiled/1 loads the module from disk
|
||||
# if it hasn't been loaded yet, so ensure_loaded? will return true in with_igniter.
|
||||
Code.ensure_compiled(Igniter)
|
||||
|
||||
import Mix.Tasks.PhoenixVite.Install.Helper
|
||||
|
||||
with_igniter do
|
||||
|
|
@ -75,25 +83,44 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
if String.contains?(content, "import LiveSvelte") do
|
||||
content
|
||||
else
|
||||
# Add after the use Gettext or import ...Gettext line in html_helpers
|
||||
web_module_name = web_module |> Module.split() |> Enum.join(".")
|
||||
|
||||
# Primary anchor: `import Phoenix.HTML` is unique to the html_helpers block.
|
||||
# Using `use Gettext` as anchor is unsafe — it appears in both controller/0
|
||||
# and html_helpers/0, causing `import LiveSvelte` to be injected in both.
|
||||
result =
|
||||
String.replace(
|
||||
content,
|
||||
~r/(use Gettext, backend: #{Regex.escape(web_module_name)}\.Gettext)/,
|
||||
"\\1\n\n import LiveSvelte"
|
||||
"import Phoenix.HTML",
|
||||
"import LiveSvelte\n\n import Phoenix.HTML",
|
||||
global: false
|
||||
)
|
||||
|
||||
# Fallback: try matching import ...Gettext pattern
|
||||
if result == content do
|
||||
String.replace(
|
||||
content,
|
||||
~r/(import #{Regex.escape(web_module_name)}\.Gettext)/,
|
||||
"\\1\n\n import LiveSvelte"
|
||||
)
|
||||
else
|
||||
if result != content do
|
||||
result
|
||||
else
|
||||
# Fallback: use Gettext pattern with global: false so only the first
|
||||
# occurrence is replaced. html_helpers is not guaranteed to use
|
||||
# `use Gettext` — older Phoenix versions use `import ...Gettext`.
|
||||
web_module_name = web_module |> Module.split() |> Enum.join(".")
|
||||
|
||||
result =
|
||||
String.replace(
|
||||
content,
|
||||
~r/(use Gettext, backend: #{Regex.escape(web_module_name)}\.Gettext)/,
|
||||
"\\1\n\n import LiveSvelte",
|
||||
global: false
|
||||
)
|
||||
|
||||
if result != content do
|
||||
result
|
||||
else
|
||||
# Last resort: old-style `import ...Gettext` pattern
|
||||
String.replace(
|
||||
content,
|
||||
~r/(import #{Regex.escape(web_module_name)}\.Gettext)/,
|
||||
"\\1\n\n import LiveSvelte",
|
||||
global: false
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
|
@ -124,11 +151,25 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
end
|
||||
|
||||
defp update_live_socket_hooks(content) do
|
||||
String.replace(
|
||||
content,
|
||||
"hooks: {...colocatedHooks},",
|
||||
"hooks: {...colocatedHooks, ...getHooks(Components)},"
|
||||
)
|
||||
cond do
|
||||
String.contains?(content, "getHooks(Components)") ->
|
||||
content
|
||||
|
||||
# Phoenix 1.8+ with colocated hooks (most common target)
|
||||
String.contains?(content, "hooks: {...colocatedHooks},") ->
|
||||
String.replace(
|
||||
content,
|
||||
"hooks: {...colocatedHooks},",
|
||||
"hooks: {...colocatedHooks, ...getHooks(Components)},"
|
||||
)
|
||||
|
||||
# Fallback: older Phoenix apps with empty hooks object
|
||||
String.contains?(content, "hooks: {},") ->
|
||||
String.replace(content, "hooks: {},", "hooks: {...getHooks(Components)},")
|
||||
|
||||
true ->
|
||||
content
|
||||
end
|
||||
end
|
||||
|
||||
# Configure Tailwind to include Svelte files (add @source "../svelte"; to app.css)
|
||||
|
|
@ -139,11 +180,13 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
if String.contains?(content, "@source \"../svelte\";") do
|
||||
content
|
||||
else
|
||||
String.replace(
|
||||
content,
|
||||
"@source \"../js\";",
|
||||
~s(@source "../js";\n@source "../svelte";)
|
||||
)
|
||||
result = String.replace(content, "@source \"../js\";", ~s(@source "../js";\n@source "../svelte";))
|
||||
# Fallback: single-quote variant used by some generators
|
||||
if result == content do
|
||||
String.replace(content, "@source '../js';", ~s(@source '../js';\n@source "../svelte";))
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
|
@ -152,7 +195,9 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
end
|
||||
end
|
||||
|
||||
# Update vite.config.mjs to add Svelte plugin and liveSveltePlugin
|
||||
# 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 ->
|
||||
|
|
@ -160,7 +205,6 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
|> add_svelte_vite_imports()
|
||||
|> update_vite_optimized_deps()
|
||||
|> update_vite_plugins()
|
||||
|> add_ssr_config()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
|
@ -201,25 +245,14 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
end
|
||||
end
|
||||
|
||||
defp add_ssr_config(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
|
||||
|
||||
# Update package.json with Svelte dependencies
|
||||
# phoenix_vite.install creates assets/package.json; npm runs from assets/
|
||||
defp update_package_json_for_svelte(igniter) do
|
||||
igniter
|
||||
|> Igniter.move_file("assets/package.json", "package.json")
|
||||
|> Igniter.update_file("package.json", fn source ->
|
||||
|> Igniter.update_file("assets/package.json", fn source ->
|
||||
Rewrite.Source.update(source, :content, fn content ->
|
||||
content
|
||||
|> add_module_type()
|
||||
|> add_svelte_dependency()
|
||||
|> add_svelte_dev_dependencies()
|
||||
end)
|
||||
|
|
@ -230,11 +263,13 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
if String.contains?(content, "\"live_svelte\"") do
|
||||
content
|
||||
else
|
||||
# Add live_svelte to dependencies section
|
||||
String.replace(
|
||||
# Capture the deps prefix (e.g. "file:../deps" or "file:./deps") from the phoenix entry
|
||||
# to match whatever convention the existing package.json uses
|
||||
Regex.replace(
|
||||
~r/"phoenix":\s*"(file:[^"]*deps)\/phoenix"/,
|
||||
content,
|
||||
~s("phoenix": "file:./deps/phoenix"),
|
||||
~s("live_svelte": "file:./deps/live_svelte",\n "phoenix": "file:./deps/phoenix")
|
||||
~s("live_svelte": "\\1/live_svelte",\n "phoenix": "\\1/phoenix"),
|
||||
global: false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -243,11 +278,24 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
if String.contains?(content, "\"@sveltejs/vite-plugin-svelte\"") do
|
||||
content
|
||||
else
|
||||
String.replace(
|
||||
content,
|
||||
~s("typescript":),
|
||||
~s("@sveltejs/vite-plugin-svelte": "^5.0.0",\n "svelte": "^5.0.0",\n "typescript":)
|
||||
)
|
||||
svelte_deps = ~s("@sveltejs/vite-plugin-svelte": "^5.0.0",\n "svelte": "^5.0.0",\n )
|
||||
|
||||
result = String.replace(content, ~s("typescript":), svelte_deps <> ~s("typescript":))
|
||||
|
||||
# Fallback: insert before "vite" which is always present in phoenix_vite output
|
||||
if result == content do
|
||||
String.replace(content, ~s("vite":), svelte_deps <> ~s("vite":))
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp add_module_type(content) do
|
||||
if String.contains?(content, "\"type\": \"module\"") do
|
||||
content
|
||||
else
|
||||
String.replace(content, "{\n", "{\n \"type\": \"module\",\n", global: false)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -258,7 +306,6 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
|
||||
igniter
|
||||
|> Igniter.mkdir("assets/svelte")
|
||||
|> Igniter.mkdir("lib/#{web_folder}/live")
|
||||
|> Igniter.create_new_file("assets/js/server.js", server_js_content())
|
||||
|> Igniter.create_new_file(
|
||||
"assets/svelte/.gitignore",
|
||||
|
|
@ -266,7 +313,7 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
)
|
||||
|> Igniter.create_new_file("assets/svelte/SvelteDemo.svelte", demo_svelte_content())
|
||||
|> Igniter.create_new_file(
|
||||
"lib/#{web_folder}/live/svelte_demo_live.ex",
|
||||
"lib/#{web_folder}/svelte_demo_live.ex",
|
||||
demo_live_view_content(igniter)
|
||||
)
|
||||
end
|
||||
|
|
@ -298,7 +345,7 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
|
||||
Igniter.update_file(igniter, "mix.exs", fn source ->
|
||||
Rewrite.Source.update(source, :content, fn content ->
|
||||
if String.contains?(content, "vite build") do
|
||||
if String.contains?(content, ~s("assets.js":)) do
|
||||
content
|
||||
else
|
||||
content
|
||||
|
|
@ -316,25 +363,46 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
String.replace(
|
||||
content,
|
||||
~r/("assets\.setup":)/,
|
||||
~s("assets.js": [\n "cmd --cd assets #{pm} vite build",\n "cmd --cd assets #{pm} vite build --config vite.ssr.config.js",\n "tailwind default"\n ],\n \\1)
|
||||
~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
|
||||
# Replace esbuild references in assets.deploy with Vite commands
|
||||
String.replace(
|
||||
content,
|
||||
~r/"esbuild default[^"]*"/,
|
||||
~s("cmd --cd assets #{pm} vite build --mode production", "cmd --cd assets #{pm} vite build --config vite.ssr.config.js --mode production")
|
||||
)
|
||||
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)
|
||||
web_folder = Macro.underscore(web_module)
|
||||
web_module_name = web_module |> Module.split() |> Enum.join(".")
|
||||
router_file = Path.join(["lib", web_folder, "router.ex"])
|
||||
|
||||
Igniter.update_file(igniter, router_file, fn source ->
|
||||
|
|
@ -342,19 +410,12 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
if String.contains?(content, "live \"/svelte_demo\"") do
|
||||
content
|
||||
else
|
||||
if String.contains?(content, "live_dashboard") do
|
||||
String.replace(
|
||||
content,
|
||||
~r/(live_dashboard.*)/,
|
||||
"\\1\n live \"/svelte_demo\", #{web_module_name}.SvelteDemoLive"
|
||||
)
|
||||
else
|
||||
String.replace(
|
||||
content,
|
||||
~r/(pipe_through :browser.*)/,
|
||||
"\\1\n live \"/dev/svelte_demo\", #{web_module_name}.SvelteDemoLive"
|
||||
)
|
||||
end
|
||||
String.replace(
|
||||
content,
|
||||
~r/(pipe_through[( ]:?browser\)?.*)/,
|
||||
"\\1\n live \"/svelte_demo\", SvelteDemoLive",
|
||||
global: false
|
||||
)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
|
@ -419,9 +480,10 @@ defmodule Mix.Tasks.LiveSvelte.Install do
|
|||
"""
|
||||
import { defineConfig } from "vite"
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte"
|
||||
import liveSveltePlugin from "live_svelte/vitePlugin"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
plugins: [svelte(), liveSveltePlugin({ entrypoint: "./js/server.js" })],
|
||||
ssr: { noExternal: true },
|
||||
build: {
|
||||
ssr: "./js/server.js",
|
||||
|
|
|
|||
222
test/live_svelte/install_test.exs
Normal file
222
test/live_svelte/install_test.exs
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
defmodule Mix.Tasks.LiveSvelte.InstallTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
import Igniter.Test
|
||||
|
||||
# phx_test_project/1 runs igniter.phx.install to create a proper Phoenix
|
||||
# project skeleton — required so that phoenix_vite.install (composed by
|
||||
# live_svelte.install) can find the endpoint, web module, router, etc.
|
||||
defp run_installer(opts \\ []) do
|
||||
argv = if opts[:bun], do: ["--bun", "--yes"], else: ["--yes"]
|
||||
|
||||
phx_test_project()
|
||||
|> Igniter.compose_task("live_svelte.install", argv)
|
||||
end
|
||||
|
||||
defp file_content(igniter, path) do
|
||||
source = Rewrite.source!(igniter.rewrite, path)
|
||||
Rewrite.Source.get(source, :content)
|
||||
end
|
||||
|
||||
defp count_occurrences(string, pattern) do
|
||||
string |> String.split(pattern) |> length() |> Kernel.-(1)
|
||||
end
|
||||
|
||||
describe "H1 regression: add_live_svelte_to_html_helpers/2" do
|
||||
test "import LiveSvelte appears exactly once in web module" do
|
||||
result = run_installer()
|
||||
web_file = "lib/test_web.ex"
|
||||
content = file_content(result, web_file)
|
||||
occurrences = count_occurrences(content, "import LiveSvelte")
|
||||
|
||||
assert occurrences == 1,
|
||||
"Expected exactly 1 `import LiveSvelte`, found #{occurrences}:\n\n#{content}"
|
||||
end
|
||||
|
||||
test "import LiveSvelte is not in the controller/0 block" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "lib/test_web.ex")
|
||||
|
||||
# Split on html_helpers definition to isolate controller block
|
||||
[controller_section | _] = String.split(content, "defp html_helpers", parts: 2)
|
||||
|
||||
refute controller_section =~ "import LiveSvelte",
|
||||
"import LiveSvelte must not appear in the controller/0 block:\n\n#{controller_section}"
|
||||
end
|
||||
|
||||
test "import LiveSvelte is inside html_helpers" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "lib/test_web.ex")
|
||||
|
||||
assert content =~ ~r/defp html_helpers.*import LiveSvelte/s,
|
||||
"import LiveSvelte not found inside html_helpers block"
|
||||
end
|
||||
end
|
||||
|
||||
describe "H2 regression: update_assets_deploy_alias/2" do
|
||||
test "assets.deploy includes SSR vite build step" 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
|
||||
|
||||
test "assets.deploy SSR step uses npx by default" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "mix.exs")
|
||||
|
||||
assert content =~
|
||||
~r/npx vite build --config vite\.ssr\.config\.js --mode production/,
|
||||
"Expected npx SSR build in assets.deploy"
|
||||
end
|
||||
|
||||
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"
|
||||
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
|
||||
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"
|
||||
end
|
||||
end
|
||||
|
||||
describe "M3: update_live_socket_hooks/1 fallback" do
|
||||
test "getHooks(Components) is added to LiveSocket hooks" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "assets/js/app.js")
|
||||
|
||||
assert content =~ "getHooks(Components)",
|
||||
"getHooks(Components) not found in app.js hooks"
|
||||
end
|
||||
end
|
||||
|
||||
describe "M4: route indentation" do
|
||||
test "svelte_demo route uses 4-space indent inside scope" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "lib/test_web/router.ex")
|
||||
|
||||
assert content =~ ~r/^ live "\/svelte_demo"/m,
|
||||
"live route has wrong indentation (expected 4 spaces):\n#{content}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "config files" do
|
||||
test "config.exs sets ssr: true" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "config/config.exs")
|
||||
assert content =~ ~r/:live_svelte.*ssr.*true/s
|
||||
end
|
||||
|
||||
test "dev.exs sets ViteJS SSR module" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "config/dev.exs")
|
||||
assert content =~ "LiveSvelte.SSR.ViteJS"
|
||||
end
|
||||
|
||||
test "prod.exs sets NodeJS SSR module" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "config/prod.exs")
|
||||
assert content =~ "LiveSvelte.SSR.NodeJS"
|
||||
end
|
||||
end
|
||||
|
||||
describe "package.json" do
|
||||
test "live_svelte dependency added" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "assets/package.json")
|
||||
assert content =~ "live_svelte"
|
||||
end
|
||||
|
||||
test "svelte dev dependencies added" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "assets/package.json")
|
||||
assert content =~ "@sveltejs/vite-plugin-svelte"
|
||||
assert content =~ ~s("svelte":)
|
||||
end
|
||||
end
|
||||
|
||||
describe "application.ex" do
|
||||
test "NodeJS.Supervisor added to children" do
|
||||
result = run_installer()
|
||||
content = file_content(result, "lib/test/application.ex")
|
||||
assert content =~ "NodeJS.Supervisor"
|
||||
end
|
||||
end
|
||||
|
||||
describe "gitignore" do
|
||||
test "svelte build artifacts are gitignored" do
|
||||
result = run_installer()
|
||||
content = file_content(result, ".gitignore")
|
||||
assert content =~ "/assets/svelte/_build/"
|
||||
assert content =~ "/priv/svelte/"
|
||||
end
|
||||
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")
|
||||
end
|
||||
|
||||
test "SvelteDemo.svelte is created" do
|
||||
result = run_installer()
|
||||
assert_creates(result, "assets/svelte/SvelteDemo.svelte")
|
||||
end
|
||||
|
||||
test "svelte_demo_live.ex is created" do
|
||||
result = run_installer()
|
||||
assert_creates(result, "lib/test_web/svelte_demo_live.ex")
|
||||
end
|
||||
end
|
||||
|
||||
describe "idempotency" do
|
||||
test "running installer twice does not duplicate import LiveSvelte" do
|
||||
base = phx_test_project()
|
||||
|
||||
result =
|
||||
base
|
||||
|> Igniter.compose_task("live_svelte.install", ["--yes"])
|
||||
|> Igniter.compose_task("live_svelte.install", ["--yes"])
|
||||
|
||||
content = file_content(result, "lib/test_web.ex")
|
||||
assert count_occurrences(content, "import LiveSvelte") == 1
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue