├── test └── test_helper.exs ├── lib ├── exinertia.ex └── mix │ └── tasks │ ├── exinertia_setup.ex │ ├── exinertia_setup_routes.ex │ └── exinertia_install.ex ├── .formatter.exs ├── .gitignore ├── LICENSE.md ├── mix.exs ├── README.md └── mix.lock /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /lib/exinertia.ex: -------------------------------------------------------------------------------- 1 | defmodule Exinertia do 2 | @moduledoc """ 3 | Documentation for `Exinertia`. 4 | """ 5 | end 6 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | 19 | # Ignore package tarball (built via "mix hex.build"). 20 | exinertia-*.tar 21 | 22 | # Temporary files, for example, from tests. 23 | /tmp/ 24 | /.elixir_ls/ 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | MIT License 4 | 5 | Copyright (c) 2024 Assim Elhammouti. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Exinertia.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :exinertia, 7 | version: "0.8.2", 8 | elixir: "~> 1.18", 9 | start_permanent: Mix.env() == :prod, 10 | elixirc_paths: elixirc_paths(Mix.env()), 11 | deps: deps(), 12 | description: 13 | "A toolkit for seamlessly integrating Inertia.js with Phoenix, using Bun for JavaScript and CSS bundling", 14 | package: package(), 15 | source_url: "https://github.com/nordbeam/exinertia", 16 | docs: [ 17 | main: "readme", 18 | extras: ["README.md"] 19 | ] 20 | ] 21 | end 22 | 23 | defp elixirc_paths(:test), do: ["lib", "test/support"] 24 | defp elixirc_paths(_), do: ["lib"] 25 | 26 | def application do 27 | [ 28 | extra_applications: [:logger] 29 | ] 30 | end 31 | 32 | defp deps do 33 | [ 34 | {:igniter, "~> 0.5", optional: true}, 35 | {:ex_doc, "~> 0.34.2", only: :dev, runtime: false} 36 | ] 37 | end 38 | 39 | defp package do 40 | [ 41 | maintainers: ["Assim El Hammouti"], 42 | licenses: ["MIT"], 43 | links: %{"GitHub" => "https://github.com/nordbeam/exinertia"}, 44 | files: ~w(lib .formatter.exs mix.exs README.md LICENSE.md) 45 | ] 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExInertia 2 | 3 | ExInertia is a toolkit for seamlessly integrating Inertia.js with Phoenix, using Bun for JavaScript and CSS bundling. 4 | 5 | ## Installation 6 | 7 | 8 | ExInertia uses Igniter under the hood, so just run: 9 | 10 | ```bash 11 | # in your project directory 12 | mix archive.install hex igniter_new 13 | mix igniter.install exinertia 14 | ``` 15 | 16 | This will set up ExInertia and all necessary dependencies in your Phoenix project. 17 | 18 | ## After Installation 19 | 20 | After installation, you'll need to update your `tailwind.config.js` to include `"./js/**/*.{js,ts,jsx,tsx}"` in the content paths. 21 | 22 | ## Development 23 | 24 | Start your Phoenix server: 25 | 26 | ```bash 27 | mix phx.server 28 | ``` 29 | 30 | Visit [`localhost:4000/inertia`](http://localhost:4000/inertia) to see your Inertia-powered page. 31 | 32 | ## Frequently Asked Questions 33 | 34 | ### Why not include these installers directly in the Phoenix Inertia adapter? 35 | 36 | ExInertia makes some opinionated choices that may not suit everyone's needs. For example, we use Bun and Vite for asset bundling instead of the more commonly used esbuild. By keeping these installers separate, users have the flexibility to choose their preferred tooling while still using the core Inertia adapter. 37 | 38 | ## Learn More 39 | 40 | - [Inertia.js Documentation](https://inertiajs.com/) 41 | - [Phoenix Framework Documentation](https://hexdocs.pm/phoenix/overview.html) 42 | - [Bun Documentation](https://bun.sh/docs) 43 | 44 | ## License 45 | 46 | ExInertia is released under the MIT License. 47 | -------------------------------------------------------------------------------- /lib/mix/tasks/exinertia_setup.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Exinertia.Setup.Docs do 2 | @moduledoc false 3 | 4 | def short_doc do 5 | "Creates a new frontend for Inertia.js." 6 | end 7 | 8 | def example do 9 | "mix exinertia.setup" 10 | end 11 | 12 | def long_doc do 13 | """ 14 | #{short_doc()} 15 | 16 | This task clones the Inertia.js template and then installs its dependencies. 17 | As a final note, ensure your tailwind.config.js is updated with the proper content paths. 18 | 19 | ## Example 20 | 21 | ```bash 22 | #{example()} 23 | ``` 24 | """ 25 | end 26 | end 27 | 28 | if Code.ensure_loaded?(Igniter) do 29 | defmodule Mix.Tasks.Exinertia.Setup do 30 | @shortdoc Mix.Tasks.Exinertia.Setup.Docs.short_doc() 31 | @moduledoc Mix.Tasks.Exinertia.Setup.Docs.long_doc() 32 | 33 | use Igniter.Mix.Task 34 | 35 | # Configuration attributes 36 | @template_repo "nordbeam/exinertia-templates/templates/react-ts" 37 | @assets_dir "assets" 38 | @bun_path "_build/bun" 39 | 40 | @impl Igniter.Mix.Task 41 | def info(_argv, _composing_task) do 42 | %Igniter.Mix.Task.Info{ 43 | group: :exinertia, 44 | adds_deps: [], 45 | installs: [], 46 | example: Mix.Tasks.Exinertia.Setup.Docs.example(), 47 | only: nil, 48 | positional: [], 49 | composes: [], 50 | schema: [ 51 | force: :boolean 52 | ], 53 | defaults: [], 54 | aliases: [], 55 | required: [] 56 | } 57 | end 58 | 59 | @impl Igniter.Mix.Task 60 | def igniter(igniter, _argv) do 61 | with :ok <- Mix.Task.run("bun.install"), 62 | bun_path <- Path.expand(@bun_path, File.cwd!()), 63 | :ok <- clone_template(bun_path), 64 | :ok <- install_dependencies(bun_path) do 65 | Igniter.add_notice(igniter, """ 66 | Successfully created frontend assets in #{@assets_dir}. 67 | 68 | As a last step, update your tailwind.config.js to add "./js/**/*.{js,ts,jsx,tsx}". 69 | """) 70 | else 71 | {:error, error} when error |> is_binary() -> 72 | Igniter.add_warning(igniter, """ 73 | Failed to clone frontend template: 74 | #{error} 75 | 76 | You may need to run manually: 77 | #{@bun_path} x degit #{@template_repo} #{@assets_dir} 78 | """) 79 | 80 | {:error, error} -> 81 | Igniter.add_warning(igniter, "Failed to install bun: #{inspect(error)}") 82 | end 83 | end 84 | 85 | defp clone_template(bun_path) do 86 | case System.cmd(bun_path, ["x", "degit", "--force", @template_repo, @assets_dir]) do 87 | {_output, 0} -> 88 | :ok 89 | 90 | {error_output, _exit_code} -> 91 | {:error, error_output} 92 | end 93 | end 94 | 95 | defp install_dependencies(bun_path) do 96 | case System.cmd(bun_path, ["i"], cd: @assets_dir) do 97 | {_output, 0} -> 98 | :ok 99 | 100 | {error_output, _exit_code} -> 101 | {:error, error_output} 102 | end 103 | end 104 | end 105 | else 106 | defmodule Mix.Tasks.Exinertia.Setup do 107 | @shortdoc Mix.Tasks.Exinertia.Setup.Docs.short_doc() <> " | Install `igniter` to use" 108 | @moduledoc Mix.Tasks.Exinertia.Setup.Docs.long_doc() 109 | 110 | use Mix.Task 111 | 112 | def run(_argv) do 113 | Mix.shell().error(""" 114 | The task 'exinertia.setup' requires igniter. Please install igniter and try again. 115 | 116 | For more information, see: https://hexdocs.pm/igniter 117 | """) 118 | 119 | exit({:shutdown, 1}) 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, 3 | "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, 4 | "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, 5 | "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, 6 | "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, 7 | "igniter": {:hex, :igniter, "0.5.21", "b80e16a47cb1fe724a2113c1f2661507d9e458978c2d610aeb87b15d9c2d43e5", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "19e516b5e06d90447c74fc4fdfc14b71318c41ef966999ac6b34d038e1aa2b9c"}, 8 | "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, 9 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 10 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 11 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 12 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 13 | "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, 14 | "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, 15 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 16 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 17 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 18 | "owl": {:hex, :owl, "0.12.1", "d3146087315c4528ee32411495ba10ec88102597b638d4d1455cf9d245dfb57a", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "d7eb9746aa89c40c46b479d6c2a70b82b94993520e40f21d0b09654f23eebf35"}, 19 | "req": {:hex, :req, "0.5.8", "50d8d65279d6e343a5e46980ac2a70e97136182950833a1968b371e753f6a662", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d7fc5898a566477e174f26887821a3c5082b243885520ee4b45555f5d53f40ef"}, 20 | "rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"}, 21 | "sourceror": {:hex, :sourceror, "1.7.1", "599d78f4cc2be7d55c9c4fd0a8d772fd0478e3a50e726697c20d13d02aa056d4", [:mix], [], "hexpm", "cd6f268fe29fa00afbc535e215158680a0662b357dc784646d7dff28ac65a0fc"}, 22 | "spitfire": {:hex, :spitfire, "0.1.4", "8fe0df66e735323e4f2a56e719603391b160dd68efd922cadfbb85a2cf6c68af", [:mix], [], "hexpm", "d40d850f4ede5235084876246756b90c7bcd12994111d57c55e2e1e23ac3fe61"}, 23 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 24 | "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, 25 | } 26 | -------------------------------------------------------------------------------- /lib/mix/tasks/exinertia_setup_routes.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Exinertia.Setup.Routes.Docs do 2 | @moduledoc false 3 | 4 | def short_doc do 5 | "Configures Routes in your Phoenix project." 6 | end 7 | 8 | def example do 9 | "mix routes.install" 10 | end 11 | 12 | def long_doc do 13 | """ 14 | #{short_doc()} 15 | 16 | This installer automates the basic steps for integrating Routes into your Phoenix project. 17 | It makes changes similar to the following: 18 | 19 | 1. Add a "use Routes" call to your Phoenix router. 20 | 2. Configure Routes in config/config.exs with your router module, along with options to output TypeScript 21 | definitions and customize the routes path. 22 | 3. [Optional] Remind you to add the Routes.Watcher to your supervision tree in development so that routes 23 | are automatically regenerated when your router file changes. 24 | 25 | ## Configuration 26 | 27 | 1. **Add Routes to your Phoenix Router**: 28 | 29 | In your router file (e.g., `lib/your_app_web/router.ex`), add `use Routes`: 30 | 31 | ```elixir 32 | defmodule YourAppWeb.Router do 33 | use Phoenix.Router 34 | use Routes # Add this line 35 | 36 | # Your routes... 37 | end 38 | ``` 39 | 40 | 2. **Configure Routes in `config/config.exs`**: 41 | 42 | Specify your router module and optional settings: 43 | 44 | ```elixir 45 | config :routes, 46 | router: YourAppWeb.Router, 47 | typescript: true, # Enable TypeScript output, defaults to false 48 | routes_path: "assets/js/routes" # Optional, defaults to "assets/js" 49 | ``` 50 | 51 | 3. **[Optional] Enable live reloading of routes**: 52 | 53 | To automatically regenerate routes when your router file changes during development, add the `Routes.Watcher` 54 | to your application's supervision tree in `lib/your_app/application.ex`: 55 | 56 | ```elixir 57 | def start(_type, _args) do 58 | children = [ 59 | # ... other children 60 | ] 61 | 62 | # Add the Routes.Watcher in development environment 63 | children = if Mix.env() == :dev do 64 | children ++ [{Routes.Watcher, []}] 65 | else 66 | children 67 | end 68 | 69 | opts = [strategy: :one_for_one, name: YourApp.Supervisor] 70 | Supervisor.start_link(children, opts) 71 | end 72 | ``` 73 | 74 | ## Example 75 | 76 | ```bash 77 | #{example()} 78 | ``` 79 | """ 80 | end 81 | end 82 | 83 | if Code.ensure_loaded?(Igniter) do 84 | defmodule Mix.Tasks.Exinertia.Setup.Routes do 85 | @shortdoc Mix.Tasks.Exinertia.Setup.Routes.Docs.short_doc() 86 | @moduledoc Mix.Tasks.Exinertia.Setup.Routes.Docs.long_doc() 87 | require Igniter.Code.Common 88 | 89 | use Igniter.Mix.Task 90 | 91 | @impl Igniter.Mix.Task 92 | def info(_argv, _composing_task) do 93 | %Igniter.Mix.Task.Info{ 94 | group: :routes, 95 | # No new deps are required here so we can leave adds_deps empty 96 | adds_deps: [], 97 | installs: [], 98 | example: Mix.Tasks.Exinertia.Setup.Routes.Docs.example(), 99 | only: nil, 100 | positional: [], 101 | composes: [], 102 | schema: [ 103 | force: :boolean, 104 | yes: :boolean 105 | ], 106 | defaults: [], 107 | aliases: [], 108 | required: [] 109 | } 110 | end 111 | 112 | @impl Igniter.Mix.Task 113 | def igniter(igniter, argv) do 114 | yes = "--yes" in argv or "-y" in argv 115 | 116 | igniter 117 | |> add_routes(yes) 118 | |> update_router() 119 | |> update_config() 120 | |> add_live_reloading() 121 | |> final_instructions() 122 | end 123 | 124 | defp add_routes(igniter, yes) do 125 | igniter 126 | |> Igniter.Project.Deps.add_dep({:routes, "~> 0.1.0"}) 127 | |> Igniter.apply_and_fetch_dependencies(yes: yes, yes_to_deps: true) 128 | end 129 | 130 | # 1. Update your Phoenix router file to include "use Routes" 131 | defp update_router(igniter) do 132 | Igniter.Project.Module.find_and_update_module!( 133 | igniter, 134 | Igniter.Libs.Phoenix.web_module(igniter), 135 | fn zipper -> 136 | with {:ok, zipper} <- Igniter.Code.Function.move_to_def(zipper, :router, 0), 137 | {:ok, zipper} <- Igniter.Code.Common.move_to_do_block(zipper) do 138 | {:ok, Igniter.Code.Common.add_code(zipper, "use Routes")} 139 | end 140 | end 141 | ) 142 | end 143 | 144 | # 2. Update config/config.exs to add a :routes configuration section. 145 | defp update_config(igniter) do 146 | router_module = Igniter.Libs.Phoenix.web_module_name(igniter, "Router") 147 | 148 | igniter 149 | |> Igniter.Project.Config.configure( 150 | "config.exs", 151 | :routes, 152 | [:router], 153 | {:code, Sourceror.parse_string!(inspect(router_module))} 154 | ) 155 | |> Igniter.Project.Config.configure( 156 | "config.exs", 157 | :routes, 158 | [:typescript], 159 | {:code, Sourceror.parse_string!("true")} 160 | ) 161 | |> Igniter.Project.Config.configure( 162 | "config.exs", 163 | :routes, 164 | [:routes_path], 165 | {:code, Sourceror.parse_string!("\"assets/js/routes\"")} 166 | ) 167 | end 168 | 169 | defp add_live_reloading(igniter) do 170 | app_module = Igniter.Project.Application.app_module(igniter) 171 | 172 | igniter 173 | |> Igniter.Project.Module.find_and_update_module!( 174 | app_module, 175 | fn zipper -> 176 | with {:ok, zipper} <- Igniter.Code.Function.move_to_def(zipper, :start, 2), 177 | {:ok, zipper} <- 178 | Igniter.Code.Common.move_to_pattern(zipper, {:=, _, [{:children, _, _}, _]}), 179 | {:ok, zipper} <- 180 | Igniter.Code.Common.move_to_pattern(zipper, {:=, _, [{:opts, _, _}, _]}) do 181 | {:ok, 182 | Igniter.Code.Common.add_code( 183 | zipper, 184 | """ 185 | 186 | children = if Mix.env() == :dev do 187 | children ++ [{Routes.Watcher, []}] 188 | else 189 | children 190 | end 191 | """, 192 | placement: :before 193 | )} 194 | else 195 | _ -> {:ok, zipper} 196 | end 197 | end 198 | ) 199 | end 200 | 201 | # 3. Remind the developer to update their application's supervision tree if they desire live reloading. 202 | defp final_instructions(igniter) do 203 | Igniter.add_notice(igniter, """ 204 | Routes installation complete. 205 | 206 | Next steps: 207 | • Verify that your router file (e.g., lib/#{web_dir(igniter)}/router.ex) now includes: 208 | use Routes 209 | 210 | • Check that your config/config.exs has been updated with: 211 | 212 | config :routes, 213 | router: #{inspect(Igniter.Libs.Phoenix.web_module_name(igniter, "Router"))}, 214 | typescript: true, 215 | routes_path: "assets/js/routes" 216 | 217 | • [Optional] To enable live reloading of routes during development, add the following 218 | to your application's supervision tree (e.g., in lib/your_app/application.ex): 219 | 220 | children = if Mix.env() == :dev do 221 | children ++ [{Routes.Watcher, []}] 222 | end 223 | 224 | Happy routing! 225 | """) 226 | end 227 | 228 | defp web_dir(igniter) do 229 | igniter 230 | |> Igniter.Libs.Phoenix.web_module() 231 | |> inspect() 232 | |> Macro.underscore() 233 | end 234 | end 235 | else 236 | # Fallback if Igniter is not installed 237 | defmodule Mix.Tasks.Exinertia.Setup.Routes do 238 | @shortdoc Mix.Tasks.Exinertia.Setup.Routes.Docs.short_doc() <> " | Install `igniter` to use" 239 | @moduledoc Mix.Tasks.Exinertia.Setup.Routes.Docs.long_doc() 240 | 241 | use Mix.Task 242 | 243 | def run(_argv) do 244 | Mix.shell().error(""" 245 | The task 'exinertia.setup.routes' requires igniter. Please install igniter and try again. 246 | 247 | For more information, see: https://hexdocs.pm/igniter 248 | """) 249 | 250 | exit({:shutdown, 1}) 251 | end 252 | end 253 | end 254 | -------------------------------------------------------------------------------- /lib/mix/tasks/exinertia_install.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Exinertia.Install.Docs do 2 | @moduledoc false 3 | 4 | def short_doc do 5 | "Installs Exinertia and updates your project accordingly." 6 | end 7 | 8 | def example do 9 | "mix exinertia.install" 10 | end 11 | 12 | def long_doc do 13 | """ 14 | #{short_doc()} 15 | 16 | This installer automates the basic steps for integrating Exinertia into a Phoenix project 17 | using bun for JavaScript and CSS bundling, removing esbuild/tailwind configs, and adding 18 | a simple Inertia setup. 19 | 20 | It follows the rough steps: 21 | 22 | 1. Create a Vite manifest reader in your Web app folder. 23 | 2. Add Inertia and bun to your mix.exs, remove esbuild/tailwind if present, and run deps.get. 24 | 3. Update config.exs to configure bun. 25 | 4. Update dev.exs watchers to point to bun and remove esbuild/tailwind watchers. 26 | 5. Modify your router and add an :inertia pipeline + a test route. 27 | 6. Modify a controller to render an Inertia page. 28 | 7. Create an inertia_root.html.heex layout. 29 | 8. Modify the existing root.html.heex to reference your new Vite manifest for main.js. 30 | 9. Insert Inertia imports into lib/myapp_web.ex for your controllers/templates. 31 | 10. Update mix aliases for building/deploying assets with bun. 32 | 11. Clone the Inertia.js template from nordbeam/exinertia-templates 33 | 12. Install frontend dependencies with bun 34 | 13. Configure Tailwind content paths for JavaScript files 35 | 36 | ## Example 37 | 38 | ```bash 39 | #{example()} 40 | ``` 41 | """ 42 | end 43 | end 44 | 45 | if Code.ensure_loaded?(Igniter) do 46 | defmodule Mix.Tasks.Exinertia.Install do 47 | @shortdoc Mix.Tasks.Exinertia.Install.Docs.short_doc() 48 | @moduledoc Mix.Tasks.Exinertia.Install.Docs.long_doc() 49 | 50 | use Igniter.Mix.Task 51 | 52 | @impl Igniter.Mix.Task 53 | def info(_argv, _composing_task) do 54 | %Igniter.Mix.Task.Info{ 55 | group: :exinertia, 56 | # Our primary new deps. Note that we remove esbuild/tailwind in code below. 57 | adds_deps: [ 58 | {:inertia, "~> 2.1.0"}, 59 | {:bun, "~> 1.4"} 60 | ], 61 | # If we wanted to cascade-install from these deps, we would list them under `installs: []`. 62 | installs: [], 63 | example: Mix.Tasks.Exinertia.Install.Docs.example(), 64 | # You can restrict to :dev or :test if desired 65 | only: nil, 66 | positional: [], 67 | composes: [ 68 | "exinertia.setup" 69 | ], 70 | # We'll parse --force, --yes from the CLI 71 | schema: [ 72 | force: :boolean, 73 | yes: :boolean 74 | ], 75 | defaults: [], 76 | aliases: [], 77 | required: [] 78 | } 79 | end 80 | 81 | @impl Igniter.Mix.Task 82 | def igniter(igniter, argv) do 83 | yes = "--yes" in argv or "-y" in argv 84 | 85 | igniter 86 | |> add_env_for_app_config() 87 | |> remove_esbuild_and_tailwind() 88 | |> add_bun_and_inertia() 89 | |> fetch_and_install_deps(yes) 90 | |> create_vite_manifest_file() 91 | |> update_config_for_bun() 92 | |> update_config_for_inertia() 93 | |> update_dev_watchers() 94 | |> update_mix_aliases() 95 | |> update_router_pipeline() 96 | |> update_inertia_controller_example() 97 | |> create_inertia_root_layout() 98 | |> patch_root_layout() 99 | |> patch_layouts() 100 | |> patch_web_module() 101 | |> Igniter.add_task("exinertia.setup") 102 | end 103 | 104 | defp add_env_for_app_config(igniter) do 105 | igniter 106 | |> Igniter.Project.Config.configure( 107 | "runtime.exs", 108 | Igniter.Project.Application.app_name(igniter), 109 | [:env], 110 | {:code, Sourceror.parse_string!("config_env()")} 111 | ) 112 | end 113 | 114 | # 1. Remove esbuild and tailwind from mix.exs, if they exist 115 | defp remove_esbuild_and_tailwind(igniter) do 116 | igniter 117 | |> Igniter.Project.Deps.remove_dep("esbuild") 118 | |> Igniter.Project.Deps.remove_dep("tailwind") 119 | end 120 | 121 | defp add_bun_and_inertia(igniter) do 122 | igniter 123 | |> Igniter.Project.Deps.add_dep({:inertia, "~> 2.1.0"}) 124 | |> Igniter.Project.Deps.add_dep({:bun, "~> 1.4"}) 125 | end 126 | 127 | # 2. Create the Vite manifest reader file (my_app_web/vite.ex or manifest.ex) 128 | # (In the guide, it was recommended to place it in your app_web folder as manifest.ex.) 129 | # Below we show how you might do that using Igniter's file generator. 130 | defp create_vite_manifest_file(igniter) do 131 | file_path = Path.join(["lib", web_dir(igniter), "manifest.ex"]) 132 | code = vite_manifest_code(igniter) 133 | 134 | Igniter.create_new_file(igniter, file_path, code) 135 | end 136 | 137 | # 3. Update config.exs to configure bun, removing old esbuild/tailwind configs if present. 138 | defp update_config_for_bun(igniter) do 139 | igniter 140 | |> Igniter.Project.Config.configure( 141 | "config.exs", 142 | :bun, 143 | [:version], 144 | {:code, Sourceror.parse_string!("\"1.2.1\"")} 145 | ) 146 | |> Igniter.Project.Config.configure( 147 | "config.exs", 148 | :bun, 149 | [:dev], 150 | {:code, 151 | Sourceror.parse_string!(""" 152 | [ 153 | args: ~w(x --bun vite), 154 | cd: Path.expand("../assets", __DIR__), 155 | env: %{} 156 | ] 157 | """)} 158 | ) 159 | |> Igniter.Project.Config.configure( 160 | "config.exs", 161 | :bun, 162 | [:install], 163 | {:code, 164 | Sourceror.parse_string!(""" 165 | [ 166 | args: ~w(i), 167 | cd: Path.expand("../assets", __DIR__), 168 | env: %{} 169 | ] 170 | """)} 171 | ) 172 | |> Igniter.Project.Config.configure( 173 | "config.exs", 174 | :bun, 175 | [:build], 176 | {:code, 177 | Sourceror.parse_string!(""" 178 | [ 179 | args: ~w(x --bun vite build), 180 | cd: Path.expand("../assets", __DIR__), 181 | env: %{} 182 | ] 183 | """)} 184 | ) 185 | |> Igniter.Project.Config.configure( 186 | "config.exs", 187 | :bun, 188 | [:css], 189 | {:code, 190 | Sourceror.parse_string!(""" 191 | [ 192 | args: ~w(run tailwindcss --input=css/app.css --output=../priv/static/assets/app.css), 193 | cd: Path.expand("../assets", __DIR__), 194 | env: %{} 195 | ] 196 | """)} 197 | ) 198 | end 199 | 200 | defp update_config_for_inertia(igniter) do 201 | igniter 202 | |> Igniter.Project.Config.configure( 203 | "config.exs", 204 | :inertia, 205 | [:endpoint], 206 | {:code, Sourceror.parse_string!("#{inspect(web_module_name(igniter))}.Endpoint")} 207 | ) 208 | |> Igniter.Project.Config.configure( 209 | "config.exs", 210 | :inertia, 211 | [:static_paths], 212 | {:code, Sourceror.parse_string!("[\"/assets/app.js\"]")} 213 | ) 214 | |> Igniter.Project.Config.configure( 215 | "config.exs", 216 | :inertia, 217 | [:default_version], 218 | {:code, Sourceror.parse_string!("\"1\"")} 219 | ) 220 | |> Igniter.Project.Config.configure( 221 | "config.exs", 222 | :inertia, 223 | [:camelize_props], 224 | {:code, Sourceror.parse_string!("false")} 225 | ) 226 | |> Igniter.Project.Config.configure( 227 | "config.exs", 228 | :inertia, 229 | [:history], 230 | {:code, Sourceror.parse_string!("[encrypt: false]")} 231 | ) 232 | |> Igniter.Project.Config.configure( 233 | "config.exs", 234 | :inertia, 235 | [:ssr], 236 | {:code, Sourceror.parse_string!("false")} 237 | ) 238 | |> Igniter.Project.Config.configure( 239 | "config.exs", 240 | :inertia, 241 | [:raise_on_ssr_failure], 242 | {:code, Sourceror.parse_string!("config_env() != :prod")} 243 | ) 244 | end 245 | 246 | # 4. Edit dev.exs watchers: add watchers for bun to config :otp_app 247 | defp update_dev_watchers(igniter) do 248 | otp_app = otp_app(igniter) 249 | 250 | igniter 251 | |> Igniter.Project.Config.configure( 252 | "dev.exs", 253 | otp_app, 254 | [Igniter.Libs.Phoenix.web_module_name(igniter, "Endpoint"), :watchers], 255 | {:code, 256 | Sourceror.parse_string!(""" 257 | [ 258 | bun: {Bun, :install_and_run, [:dev, ~w()]}, 259 | bun_css: {Bun, :install_and_run, [:css, ~w(--watch)]} 260 | ] 261 | """)} 262 | ) 263 | end 264 | 265 | # 5. Edit mix.exs to add new asset aliases for bun usage: 266 | defp update_mix_aliases(igniter) do 267 | igniter 268 | |> Igniter.Project.TaskAliases.modify_existing_alias("assets.setup", fn zipper -> 269 | {:ok, 270 | Sourceror.Zipper.replace(zipper, quote(do: ["bun.install --if-missing", "bun install"]))} 271 | end) 272 | |> Igniter.Project.TaskAliases.modify_existing_alias("assets.build", fn zipper -> 273 | {:ok, 274 | Sourceror.Zipper.replace(zipper, quote(do: ["bun install", "bun build", "bun css"]))} 275 | end) 276 | |> Igniter.Project.TaskAliases.modify_existing_alias("assets.deploy", fn zipper -> 277 | {:ok, 278 | Sourceror.Zipper.replace( 279 | zipper, 280 | quote(do: ["bun install", "bun build --minify", "bun css --minify", "phx.digest"]) 281 | )} 282 | end) 283 | end 284 | 285 | # 6. Add "pipeline :inertia" to router plus a small example route: 286 | defp update_router_pipeline(igniter) do 287 | inertia_pipeline = """ 288 | plug :accepts, ["html"] 289 | plug :fetch_session 290 | plug :fetch_live_flash 291 | plug :put_root_layout, html: {#{inspect(web_module_name(igniter))}.Layouts, :inertia_root} 292 | plug :protect_from_forgery 293 | plug :put_secure_browser_headers 294 | plug Inertia.Plug 295 | """ 296 | 297 | igniter 298 | |> Igniter.Libs.Phoenix.add_pipeline( 299 | :inertia, 300 | inertia_pipeline, 301 | arg2: web_module_name(igniter) 302 | ) 303 | |> Igniter.Libs.Phoenix.add_scope( 304 | "/inertia", 305 | """ 306 | pipe_through :inertia 307 | get "/", PageController, :inertia 308 | """, 309 | arg2: web_module_name(igniter) 310 | ) 311 | end 312 | 313 | # 7. Update a controller (PageController) with an inertia action. We'll do a naive insertion: 314 | defp update_inertia_controller_example(igniter) do 315 | Igniter.Project.Module.find_and_update_module!( 316 | igniter, 317 | Igniter.Libs.Phoenix.web_module_name(igniter, "PageController"), 318 | fn zipper -> 319 | inertia_fn = """ 320 | def inertia(conn, _params) do 321 | conn 322 | |> render_inertia("Dashboard") 323 | end 324 | """ 325 | 326 | {:ok, Igniter.Code.Common.add_code(zipper, inertia_fn)} 327 | end 328 | ) 329 | end 330 | 331 | # 8. Create an inertia_root.html.heex in MyAppWeb.Components.Layouts 332 | defp create_inertia_root_layout(igniter) do 333 | file_path = 334 | Path.join([ 335 | "lib", 336 | web_dir(igniter), 337 | "components", 338 | "layouts", 339 | "inertia_root.html.heex" 340 | ]) 341 | 342 | content = inertia_root_html() 343 | 344 | Igniter.create_new_file(igniter, file_path, content) 345 | end 346 | 347 | # 9. Patch the existing root.html.heex to reference your Vite manifest for main_js 348 | defp patch_root_layout(igniter) do 349 | file_path = 350 | Path.join([ 351 | "lib", 352 | web_dir(igniter), 353 | "components", 354 | "layouts", 355 | "root.html.heex" 356 | ]) 357 | 358 | script_snippet = """ 359 | <%= if dev_env?() do %> 360 | 362 | <% else %> 363 | 365 | <% end %> 366 | """ 367 | 368 | updater = fn source -> 369 | source 370 | |> Rewrite.Source.update(:content, fn content -> 371 | content 372 | |> String.replace(~r/]*>.*?<\/script>/s, "") 373 | |> String.replace("", "#{script_snippet}\n ") 374 | end) 375 | end 376 | 377 | Igniter.update_file(igniter, file_path, updater) 378 | end 379 | 380 | defp patch_layouts(igniter) do 381 | Igniter.Project.Module.find_and_update_module!( 382 | igniter, 383 | Igniter.Libs.Phoenix.web_module_name(igniter, "Layouts"), 384 | fn zipper -> 385 | {:ok, 386 | Igniter.Code.Common.add_code(zipper, """ 387 | def dev_env?, do: Application.get_env(:#{Igniter.Project.Application.app_name(igniter)}, :env) == :dev 388 | """)} 389 | end 390 | ) 391 | end 392 | 393 | # 10. Patch lib/myapp_web.ex to import Inertia.Controller and Inertia.HTML in the relevant blocks 394 | defp patch_web_module(igniter) do 395 | igniter = 396 | Igniter.Project.Module.find_and_update_module!( 397 | igniter, 398 | web_module_name(igniter), 399 | fn zipper -> 400 | with {:ok, zipper} <- Igniter.Code.Function.move_to_def(zipper, :controller, 0), 401 | {:ok, zipper} <- Igniter.Code.Common.move_to_do_block(zipper) do 402 | {:ok, Igniter.Code.Common.add_code(zipper, "import Inertia.Controller")} 403 | end 404 | end 405 | ) 406 | 407 | Igniter.Project.Module.find_and_update_module!( 408 | igniter, 409 | web_module_name(igniter), 410 | fn zipper -> 411 | with {:ok, zipper} <- Igniter.Code.Function.move_to_def(zipper, :html, 0), 412 | {:ok, zipper} <- Igniter.Code.Common.move_to_do_block(zipper) do 413 | {:ok, Igniter.Code.Common.add_code(zipper, "import Inertia.HTML")} 414 | end 415 | end 416 | ) 417 | end 418 | 419 | # After applying most of our changes, run "mix deps.get" and then "mix bun.install" 420 | defp fetch_and_install_deps(igniter, yes) do 421 | Igniter.apply_and_fetch_dependencies(igniter, yes: yes, yes_to_deps: true) 422 | end 423 | 424 | # 425 | # Helper functions 426 | # 427 | 428 | # Return the code for the Vite manifest module, adapted from your example 429 | defp vite_manifest_code(igniter) do 430 | """ 431 | defmodule Vite do 432 | @moduledoc false 433 | 434 | # Provide "constants" as functions so that inner modules can refer to them. 435 | def manifest_file, do: "priv/static/assets/vite_manifest.json" 436 | def cache_key, do: {:vite, "vite_manifest"} 437 | def default_env, do: :dev 438 | def endpoint, do: #{inspect(web_module_name(igniter))}.Endpoint 439 | 440 | defmodule PhxManifestReader do 441 | @moduledoc \"\"\" 442 | Reads Vite manifest data either from a built digest (for prod) or directly from disk (for non-prod). 443 | \"\"\" 444 | require Logger 445 | alias Vite 446 | 447 | @spec read() :: map() 448 | def read do 449 | case :persistent_term.get(Vite.cache_key(), nil) do 450 | nil -> 451 | manifest = do_read(current_env()) 452 | :persistent_term.put(Vite.cache_key(), manifest) 453 | manifest 454 | 455 | manifest -> 456 | manifest 457 | end 458 | end 459 | 460 | @spec current_env() :: atom() 461 | def current_env do 462 | Application.get_env(:#{otp_app(igniter)}, :env, Vite.default_env()) 463 | end 464 | 465 | @spec do_read(atom()) :: map() 466 | defp do_read(:prod), do: read_prod_manifest() 467 | defp do_read(_env), do: read_file_manifest(Vite.manifest_file()) 468 | 469 | # Reads the manifest file from the built static digest in production. 470 | @spec read_prod_manifest() :: map() 471 | defp read_prod_manifest do 472 | # In production the manifest location is picked up from the parent's manifest_file/0. 473 | 474 | {otp_app, relative_path} = {Vite.endpoint().config(:otp_app), Vite.manifest_file()} 475 | 476 | manifest_path = Application.app_dir(otp_app, relative_path) 477 | 478 | with true <- File.exists?(manifest_path), 479 | {:ok, content} <- File.read(manifest_path), 480 | {:ok, decoded} <- Phoenix.json_library().decode(content) do 481 | decoded 482 | else 483 | _ -> 484 | Logger.error( 485 | "Could not find static manifest at \#{inspect(manifest_path)}. " <> 486 | "Run \\"mix phx.digest\\" after building your static files " <> 487 | "or remove the configuration from \\"config/prod.exs\\"." 488 | ) 489 | %{} 490 | end 491 | end 492 | 493 | # Reads the manifest from a file for non-production environments. 494 | @spec read_file_manifest(String.t()) :: map() 495 | defp read_file_manifest(path) do 496 | path 497 | |> File.read!() 498 | |> Jason.decode!() 499 | end 500 | end 501 | 502 | defmodule Manifest do 503 | @moduledoc \"\"\" 504 | Retrieves Vite's generated file references. 505 | \"\"\" 506 | alias Vite.PhxManifestReader 507 | 508 | @main_js_file "js/app.js" 509 | @inertia_js_file "js/inertia.tsx" 510 | 511 | @spec read() :: map() 512 | def read, do: PhxManifestReader.read() 513 | 514 | @doc "Returns the main JavaScript file path prepended with a slash." 515 | @spec main_js() :: String.t() 516 | def main_js, do: get_file(@main_js_file) 517 | 518 | @doc "Returns the inertia JavaScript file path prepended with a slash." 519 | @spec inertia_js() :: String.t() 520 | def inertia_js, do: get_file(@inertia_js_file) 521 | 522 | @doc "Returns the main CSS file path prepended with a slash, if available." 523 | @spec main_css() :: String.t() 524 | def main_css, do: get_css(@main_js_file) 525 | 526 | @spec get_file(String.t()) :: String.t() 527 | def get_file(file) do 528 | read() 529 | |> get_in([file, "file"]) 530 | |> prepend_slash() 531 | end 532 | 533 | @spec get_css(String.t()) :: String.t() 534 | def get_css(file) do 535 | read() 536 | |> get_in([file, "css"]) 537 | |> List.first() 538 | |> prepend_slash() 539 | end 540 | 541 | @doc \"\"\" 542 | Returns the list of import paths for a given file, 543 | each path is prepended with a slash. 544 | \"\"\" 545 | @spec get_imports(String.t()) :: [String.t()] 546 | def get_imports(file) do 547 | read() 548 | |> get_in([file, "imports"]) 549 | |> case do 550 | nil -> [] 551 | imports -> Enum.map(imports, &get_file/1) 552 | end 553 | end 554 | 555 | defp prepend_slash(nil), do: "" 556 | defp prepend_slash(path) when is_binary(path), do: "/" <> path 557 | defp prepend_slash(_), do: "" 558 | end 559 | end 560 | """ 561 | end 562 | 563 | # The inertia_root.html.heex layout content 564 | defp inertia_root_html do 565 | """ 566 | 567 | 568 | 569 | 570 | 571 | 572 | <.inertia_title><%= assigns[:page_title] %> 573 | <.inertia_head content={@inertia_head} /> 574 | 575 | <%= if dev_env?() do %> 576 | 583 | 584 | 585 | <% else %> 586 | 588 | <% end %> 589 | 590 | 591 | <%= @inner_content %> 592 | 593 | 594 | """ 595 | end 596 | 597 | # Helpers to get the OTP and Web module names from the project: 598 | defp otp_app(igniter) do 599 | Igniter.Project.Application.app_name(igniter) 600 | end 601 | 602 | defp web_module_name(igniter) do 603 | Igniter.Libs.Phoenix.web_module(igniter) 604 | end 605 | 606 | defp web_dir(igniter) do 607 | igniter 608 | |> Igniter.Libs.Phoenix.web_module() 609 | |> inspect() 610 | |> Macro.underscore() 611 | end 612 | end 613 | else 614 | # Fallback if Igniter is not installed 615 | defmodule Mix.Tasks.Exinertia.Install do 616 | @shortdoc Mix.Tasks.Exinertia.Install.Docs.short_doc() <> " | Install `igniter` to use" 617 | @moduledoc Mix.Tasks.Exinertia.Install.Docs.long_doc() 618 | 619 | use Mix.Task 620 | 621 | def run(_argv) do 622 | Mix.shell().error(""" 623 | The task 'exinertia.install' requires igniter. Please install igniter and try again. 624 | 625 | For more information, see: https://hexdocs.pm/igniter 626 | """) 627 | 628 | exit({:shutdown, 1}) 629 | end 630 | end 631 | end 632 | --------------------------------------------------------------------------------