diff --git a/assets/copy/build.js b/assets/copy/build.js deleted file mode 100644 index 0447c41..0000000 --- a/assets/copy/build.js +++ /dev/null @@ -1,71 +0,0 @@ -const esbuild = require("esbuild") -const sveltePlugin = require("esbuild-svelte") -const importGlobPlugin = require("esbuild-plugin-import-glob").default -const sveltePreprocess = require("svelte-preprocess") - -const args = process.argv.slice(2) -const watch = args.includes("--watch") -const deploy = args.includes("--deploy") - -let clientConditions = ["svelte", "browser"] -let serverConditions = ["svelte"] - -if (!deploy) { - clientConditions.push("development") - serverConditions.push("development") -} - -let optsClient = { - entryPoints: ["js/app.js"], - bundle: true, - minify: deploy, - conditions: clientConditions, - alias: {svelte: "svelte"}, - outdir: "../priv/static/assets/js", - logLevel: "info", - sourcemap: watch ? "inline" : false, - tsconfig: "./tsconfig.json", - plugins: [ - importGlobPlugin(), - sveltePlugin({ - preprocess: sveltePreprocess(), - compilerOptions: {dev: !deploy, css: "injected", generate: "client"}, - }), - ], -} - -let optsServer = { - entryPoints: ["js/server.js"], - platform: "node", - bundle: true, - minify: false, - target: "node19.6.1", - conditions: serverConditions, - alias: {svelte: "svelte"}, - outdir: "../priv/svelte", - logLevel: "info", - sourcemap: watch ? "inline" : false, - tsconfig: "./tsconfig.json", - plugins: [ - importGlobPlugin(), - sveltePlugin({ - preprocess: sveltePreprocess(), - compilerOptions: {dev: !deploy, css: "injected", generate: "server"}, - }), - ], -} - -if (watch) { - esbuild - .context(optsClient) - .then(ctx => ctx.watch()) - .catch(_error => process.exit(1)) - - esbuild - .context(optsServer) - .then(ctx => ctx.watch()) - .catch(_error => process.exit(1)) -} else { - esbuild.build(optsClient) - esbuild.build(optsServer) -} diff --git a/assets/copy/js/app.js b/assets/copy/js/app.js deleted file mode 100644 index 6524991..0000000 --- a/assets/copy/js/app.js +++ /dev/null @@ -1,42 +0,0 @@ -// If you want to use Phoenix channels, run `mix help phx.gen.channel` -// to get started and then uncomment the line below. -// import "./user_socket.js" - -// You can include dependencies in two ways. -// -// The simplest option is to put them in assets/vendor and -// import them using relative paths: -// -// import "../vendor/some-package.js" -// -// Alternatively, you can `npm install some-package --prefix assets` and import -// them using a path starting with the package name: -// -// import "some-package" -// - -// Include phoenix_html to handle method=PUT/DELETE in forms and buttons. -import "phoenix_html" -// Establish Phoenix Socket and LiveView configuration. -import {Socket} from "phoenix" -import {LiveSocket} from "phoenix_live_view" -import topbar from "../vendor/topbar" -import {getHooks} from "live_svelte" -import * as Components from "../svelte/**/*.svelte" - -let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") -let liveSocket = new LiveSocket("/live", Socket, {hooks: getHooks(Components), params: {_csrf_token: csrfToken}}) - -// Show progress bar on live navigation and form submits -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()) - -// connect if there are any LiveViews on the page -liveSocket.connect() - -// expose liveSocket on window for web console debug logs and latency simulation: -// >> liveSocket.enableDebug() -// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session -// >> liveSocket.disableLatencySim() -window.liveSocket = liveSocket diff --git a/assets/copy/js/server.js b/assets/copy/js/server.js deleted file mode 100644 index 9157f7c..0000000 --- a/assets/copy/js/server.js +++ /dev/null @@ -1,4 +0,0 @@ -import * as Components from "../svelte/**/*.svelte" -import {getRender} from "live_svelte" - -export const render = getRender(Components) diff --git a/example_project/mix.lock b/example_project/mix.lock index 1c92393..ef5bdb8 100644 --- a/example_project/mix.lock +++ b/example_project/mix.lock @@ -38,6 +38,7 @@ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_test": {:hex, :phoenix_test, "0.9.1", "ac58a4d341c594ac57ce52a6ce643200084fad419a91b72896a44881fe84809c", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:lazy_html, "~> 0.1.7", [hex: :lazy_html, repo: "hexpm", optional: false]}, {:mime, ">= 1.0.0", [hex: :mime, repo: "hexpm", optional: true]}, {:phoenix, ">= 1.7.10", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "ed453394c0f8987aa58a06e2302e7dd4bc53cd2d25eff5a18c4a5775241ebe61"}, + "phoenix_vite": {:hex, :phoenix_vite, "0.4.1", "16fc8b4acc7d4e26fab2bcfb10d125392fcfb5a4ba05dd3aea7f2bec048c5e71", [:mix], [{:bun, ">= 1.5.1 and < 2.0.0-0", [hex: :bun, repo: "hexpm", optional: true]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "f2bdb1802bc82f2fa93b24606cd25ebdd389bcf2afc55af804f0af3377410f0d"}, "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, diff --git a/lib/logger.ex b/lib/logger.ex deleted file mode 100644 index 9702b5e..0000000 --- a/lib/logger.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule LiveSvelte.Logger do - @moduledoc false - - @doc false - def log_info(status), do: Mix.shell().info([status, :reset]) - - @doc false - def log_success(status), do: Mix.shell().info([:green, status, :reset]) - - @doc false - def log_warning(status), do: Mix.shell().info([:yellow, status, :reset]) - - @doc false - def log_error(status), do: Mix.shell().error([status, :reset]) -end diff --git a/lib/mix/tasks/configure_esbuild.ex b/lib/mix/tasks/configure_esbuild.ex deleted file mode 100644 index 75d52c9..0000000 --- a/lib/mix/tasks/configure_esbuild.ex +++ /dev/null @@ -1,23 +0,0 @@ -defmodule Mix.Tasks.LiveSvelte.ConfigureEsbuild do - @moduledoc """ - Creates Javascript files to be used by esbuild. Necessary for LiveSvelte to work. - """ - - import LiveSvelte.Logger - - def run(_) do - log_info("-- Configuring esbuild...") - - Mix.Project.deps_paths(depth: 1) - |> Map.fetch!(:live_svelte) - |> Path.join("assets/copy/**/*.{js,json}") - |> Path.wildcard() - |> Enum.each(fn full_path -> - [_beginning, relative_path] = String.split(full_path, "copy", parts: 2) - - Mix.Generator.copy_file(full_path, "assets" <> relative_path) - end) - - Mix.Generator.create_directory("assets/svelte") - end -end diff --git a/lib/mix/tasks/configure_phoenix.ex b/lib/mix/tasks/configure_phoenix.ex deleted file mode 100644 index edb1c6c..0000000 --- a/lib/mix/tasks/configure_phoenix.ex +++ /dev/null @@ -1,116 +0,0 @@ -defmodule Mix.Tasks.LiveSvelte.ConfigurePhoenix do - @moduledoc """ - Configures any necessary code changes inside Phoenix to make LiveSvelte work. - """ - - import LiveSvelte.Logger - - @watcher_regex ~r/watchers:\s\[(?!\s+node:)/ - @esbuild_regex ~r/(? - err - |> Exception.message() - |> log_error() - end - - Mix.Task.run("format") - end - - defp configure_dev_config() do - text = ~s""" - node: ["build.js", "--watch", cd: Path.expand("../assets", __DIR__)],\ - """ - - {path, file} = path_and_file("config/", "dev.exs") - - File.read!(path) - |> insert(@watcher_regex, text, "'#{text}' in #{file}") - |> comment(@esbuild_regex, "old esbuild watcher in #{file}") - |> save(path) - end - - defp configure_application() do - text = ~s""" - {NodeJS.Supervisor, [path: LiveSvelte.SSR.NodeJS.server_path(), pool_size: 4]},\ - """ - - {path, file} = path_and_file("lib/**/", "application.ex") - - File.read!(path) - |> insert(@nodejs_regex, text, "'#{text}' in #{file}") - |> save(path) - end - - defp configure_gitignore() do - text = ~s""" - - - # Ignore automatically generated Svelte files by the ~V sigil - /assets/svelte/_build/ - - # Ignore ssr build for svelte. - /priv/svelte/\ - """ - - {path, file} = path_and_file("", ".gitignore") - - File.read!(path) - |> insert(@gitignore_regex, text, "'#{text}' in #{file}") - |> save(path) - end - - defp path_and_file(wildcard, filename) do - {find_file("#{wildcard}#{filename}", filename), filename} - end - - defp find_file(wildcard, file_name) do - with [path] <- Path.wildcard(wildcard) do - path - else - [] -> raise "Could not find #{file_name}" - [_ | _] -> raise "Found multiple #{file_name} files" - end - end - - defp insert(source, regex, to_insert, name) do - case Regex.run(regex, source, return: :index) do - [{pos, len}] -> - log_success("Inserted #{name}") - insert_position(source, pos + len, to_insert) - - nil -> - log_error("Could not insert #{name}, please do so yourself") - source - end - end - - defp comment(source, regex, name) do - case Regex.run(regex, source, return: :index) do - [{pos, _len}] -> - log_success("Commented out #{name}") - insert_position(source, pos, "# ") - - nil -> - log_warning("Could not comment out #{name}") - source - end - end - - defp insert_position(source, position, to_insert) do - {head, tail} = String.split_at(source, position) - head <> to_insert <> tail - end - - defp save(source, target_file), do: File.write!(target_file, source) -end diff --git a/lib/mix/tasks/install_npm_deps.ex b/lib/mix/tasks/install_npm_deps.ex deleted file mode 100644 index b294de1..0000000 --- a/lib/mix/tasks/install_npm_deps.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule Mix.Tasks.LiveSvelte.InstallNpmDeps do - @moduledoc """ - Installs npm dependencies for LiveSvelte. - """ - - import LiveSvelte.Logger - - def run(_) do - log_info("-- Installing npm dependencies...") - - "npm install --prefix ./assets --save-dev esbuild esbuild-svelte svelte svelte-preprocess esbuild-plugin-import-glob && - npm install --prefix ./assets --save ./deps/phoenix ./deps/phoenix_html ./deps/phoenix_live_view ./deps/live_svelte" - |> String.to_charlist() - |> :os.cmd() - |> IO.puts() - end -end diff --git a/lib/mix/tasks/live_svelte.install.ex b/lib/mix/tasks/live_svelte.install.ex new file mode 100644 index 0000000..0584384 --- /dev/null +++ b/lib/mix/tasks/live_svelte.install.ex @@ -0,0 +1,497 @@ +defmodule Mix.Tasks.LiveSvelte.Install do + @moduledoc """ + Installer for LiveSvelte with Vite. + + This task first installs Vite using the PhoenixVite installer, + then configures the project for LiveSvelte. + + ## Options + + * `--bun` - Use Bun instead of Node.js/npm + + ## Examples + + mix igniter.install live_svelte + mix igniter.install live_svelte --bun + + """ + + import Mix.Tasks.PhoenixVite.Install.Helper + + with_igniter do + use Igniter.Mix.Task + + alias Igniter.Libs.Phoenix + alias Igniter.Project.Config + + @impl Igniter.Mix.Task + def info(_argv, _parent) do + %Igniter.Mix.Task.Info{ + composes: ["phoenix_vite.install"], + schema: [bun: :boolean], + aliases: [b: :bun] + } + end + + @impl Igniter.Mix.Task + def igniter(igniter) do + app_name = Igniter.Project.Application.app_name(igniter) + + igniter + |> Igniter.compose_task("phoenix_vite.install", igniter.args.argv) + |> configure_environments(app_name) + |> add_live_svelte_to_html_helpers(app_name) + |> update_javascript_configuration() + |> configure_tailwind_for_svelte() + |> update_vite_configuration() + |> update_package_json_for_svelte() + |> create_svelte_files() + |> setup_ssr_for_production(app_name) + |> update_mix_aliases() + |> add_svelte_demo_route() + |> update_home_template() + |> update_gitignore() + |> create_ssr_vite_config() + end + + # Configure environments (config.exs, dev.exs, prod.exs) + defp configure_environments(igniter, _app_name) do + igniter + |> Config.configure("config.exs", :live_svelte, [:ssr], true) + |> Config.configure("dev.exs", :live_svelte, [:ssr_module], {:code, Sourceror.parse_string!("LiveSvelte.SSR.ViteJS")}) + |> Config.configure("dev.exs", :live_svelte, [:vite_host], "http://localhost:5173") + |> Config.configure("prod.exs", :live_svelte, [:ssr_module], {:code, Sourceror.parse_string!("LiveSvelte.SSR.NodeJS")}) + |> Config.configure("prod.exs", :live_svelte, [:ssr], true) + end + + # Add import LiveSvelte to html_helpers in lib/app_web.ex + defp add_live_svelte_to_html_helpers(igniter, _app_name) do + web_module = Phoenix.web_module(igniter) + web_folder = Macro.underscore(web_module) + web_file = Path.join(["lib", web_folder <> ".ex"]) + + Igniter.update_file(igniter, web_file, fn source -> + Rewrite.Source.update(source, :content, fn content -> + 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(".") + + result = + String.replace( + content, + ~r/(use Gettext, backend: #{Regex.escape(web_module_name)}\.Gettext)/, + "\\1\n\n import LiveSvelte" + ) + + # 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 + result + end + end + end) + end) + end + + # Update app.js to import getHooks and Components from live_svelte + defp update_javascript_configuration(igniter) do + Igniter.update_file(igniter, "assets/js/app.js", fn source -> + Rewrite.Source.update(source, :content, fn content -> + content + |> add_live_svelte_imports() + |> update_live_socket_hooks() + end) + end) + end + + defp add_live_svelte_imports(content) do + if String.contains?(content, "import {getHooks} from \"live_svelte\"") do + content + else + String.replace( + content, + "import topbar from \"topbar\"", + ~s(import topbar from "topbar"\nimport {getHooks} from "live_svelte"\nimport Components from "virtual:live-svelte-components") + ) + end + end + + defp update_live_socket_hooks(content) do + String.replace( + content, + "hooks: {...colocatedHooks},", + "hooks: {...colocatedHooks, ...getHooks(Components)}," + ) + end + + # Configure Tailwind to include Svelte files (add @source "../svelte"; to app.css) + defp configure_tailwind_for_svelte(igniter) do + if Igniter.exists?(igniter, "assets/css/app.css") do + Igniter.update_file(igniter, "assets/css/app.css", fn source -> + Rewrite.Source.update(source, :content, fn content -> + if String.contains?(content, "@source \"../svelte\";") do + content + else + String.replace( + content, + "@source \"../js\";", + ~s(@source "../js";\n@source "../svelte";) + ) + end + end) + end) + else + igniter + end + end + + # Update vite.config.mjs to add Svelte plugin and liveSveltePlugin + defp update_vite_configuration(igniter) do + Igniter.update_file(igniter, "assets/vite.config.mjs", fn source -> + Rewrite.Source.update(source, :content, fn content -> + content + |> add_svelte_vite_imports() + |> update_vite_optimized_deps() + |> update_vite_plugins() + |> add_ssr_config() + end) + end) + end + + defp add_svelte_vite_imports(content) do + if String.contains?(content, "import { svelte }") do + content + else + String.replace( + content, + "import { phoenixVitePlugin } from 'phoenix_vite'", + ~s(import { svelte } from "@sveltejs/vite-plugin-svelte"\nimport liveSveltePlugin from "live_svelte/vitePlugin") + ) + end + end + + defp update_vite_optimized_deps(content) do + if String.contains?(content, "\"live_svelte\"") do + content + else + String.replace( + content, + ~s(include: ["phoenix", "phoenix_html", "phoenix_live_view"],), + ~s(include: ["live_svelte", "phoenix", "phoenix_html", "phoenix_live_view"],) + ) + end + end + + defp update_vite_plugins(content) do + if String.contains?(content, "svelte(") do + content + else + String.replace( + content, + ~r/phoenixVitePlugin\(\{\s*pattern: \/\\.\(ex\|heex\)\$\/\s*\}\)/s, + "svelte({ compilerOptions: { css: \"injected\" } }),\n liveSveltePlugin({ entrypoint: \"./js/server.js\" })" + ) + 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 + defp update_package_json_for_svelte(igniter) do + igniter + |> Igniter.move_file("assets/package.json", "package.json") + |> Igniter.update_file("package.json", fn source -> + Rewrite.Source.update(source, :content, fn content -> + content + |> add_svelte_dependency() + |> add_svelte_dev_dependencies() + end) + end) + end + + defp add_svelte_dependency(content) do + if String.contains?(content, "\"live_svelte\"") do + content + else + # Add live_svelte to dependencies section + String.replace( + content, + ~s("phoenix": "file:./deps/phoenix"), + ~s("live_svelte": "file:./deps/live_svelte",\n "phoenix": "file:./deps/phoenix") + ) + end + end + + defp add_svelte_dev_dependencies(content) 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":) + ) + end + end + + # Create Svelte project files + defp create_svelte_files(igniter) do + web_module = Phoenix.web_module(igniter) + web_folder = Macro.underscore(web_module) + + 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", + "# Ignore auto-generated Svelte files by ~V sigil\n_build/" + ) + |> Igniter.create_new_file("assets/svelte/SvelteDemo.svelte", demo_svelte_content()) + |> Igniter.create_new_file( + "lib/#{web_folder}/live/svelte_demo_live.ex", + demo_live_view_content(igniter) + ) + end + + # Setup NodeJS SSR supervisor in application.ex + defp setup_ssr_for_production(igniter, _app_name) do + app_module = igniter |> Igniter.Project.Application.app_name() |> to_string() + app_file = "lib/#{Macro.underscore(app_module)}/application.ex" + + Igniter.update_file(igniter, app_file, fn source -> + Rewrite.Source.update(source, :content, fn content -> + if String.contains?(content, "children = [") and not String.contains?(content, "NodeJS.Supervisor") do + String.replace( + content, + ~r/(children = \[\s*\n)/, + "\\1 {NodeJS.Supervisor, [path: LiveSvelte.SSR.NodeJS.server_path(), pool_size: 4]},\n" + ) + else + content + end + end) + end) + end + + # Update mix.exs aliases in the consumer app to use Vite + 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, "vite build") do + content + else + content + |> add_assets_js_alias(pm) + |> update_assets_deploy_alias(pm) + 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 "cmd --cd assets #{pm} vite build",\n "cmd --cd assets #{pm} vite build --config vite.ssr.config.js",\n "tailwind default"\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") + ) + 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 -> + Rewrite.Source.update(source, :content, fn content -> + 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 + end + end) + end) + end + + # Update home template with LiveSvelte info + defp update_home_template(igniter) do + web_module = Phoenix.web_module(igniter) + web_folder = Macro.underscore(web_module) + home_template = Path.join(["lib", web_folder, "controllers", "page_html", "home.html.heex"]) + + Igniter.update_file(igniter, home_template, fn source -> + Rewrite.Source.update(source, :content, fn content -> + content + |> String.replace( + "Peace of mind from prototype to production.", + "End-to-end reactivity for your Live Svelte apps." + ) + |> String.replace( + ~s(
Count: {count}
+ + +