chore: improve igniter installer

This commit is contained in:
Denis Donici 2026-03-05 19:03:32 +02:00
parent 3f2684be40
commit beb462fafb
5 changed files with 564 additions and 102 deletions

220
README.md
View file

@ -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

View file

@ -1,4 +1,5 @@
{
"type": "module",
"devDependencies": {
"@vitest/coverage-v8": "^2.1.0",
"esbuild": "^0.24.0",

View 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>

View file

@ -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",

View 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