diff --git a/README.md b/README.md index e9a64e6..64a9fe0 100644 --- a/README.md +++ b/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 --minify", "cmd --cd assets npx vite build", "phx.digest"] - ] -end -``` - -Note: `tailwind --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/_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/_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/_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//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 diff --git a/assets/package.json b/assets/package.json index be38148..19fa248 100644 --- a/assets/package.json +++ b/assets/package.json @@ -1,4 +1,5 @@ { + "type": "module", "devDependencies": { "@vitest/coverage-v8": "^2.1.0", "esbuild": "^0.24.0", diff --git a/example_project/assets/svelte/Nested.svelte b/example_project/assets/svelte/Nested.svelte new file mode 100644 index 0000000..931264d --- /dev/null +++ b/example_project/assets/svelte/Nested.svelte @@ -0,0 +1,15 @@ + + +
+

Parent Svelte component

+
+ alert("Event triggered in child and handled in parent")} /> +
+
Slot Item
+ {@render children?.()} +
+
+
diff --git a/lib/mix/tasks/live_svelte.install.ex b/lib/mix/tasks/live_svelte.install.ex index 0584384..dfc6957 100644 --- a/lib/mix/tasks/live_svelte.install.ex +++ b/lib/mix/tasks/live_svelte.install.ex @@ -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", diff --git a/test/live_svelte/install_test.exs b/test/live_svelte/install_test.exs new file mode 100644 index 0000000..c0b57a7 --- /dev/null +++ b/test/live_svelte/install_test.exs @@ -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