Current section
Files
Jump to
Current section
Files
lib/mix/tasks/inertia.install.ex
defmodule Mix.Tasks.Inertia.Install.Docs do
@moduledoc false
def short_doc do
"Installs and configures the Inertia.js adapter in a Phoenix application."
end
def example do
"mix inertia.install"
end
def long_doc do
"""
Installs and configures the Inertia.js adapter in a Phoenix application.
This installer:
1. Updates controller and HTML components to import Inertia functions
2. Adds the Inertia plug to the browser pipeline
3. Adds basic configuration in config.exs
4. Updates esbuild and configures code splitting
5. Set up the client-side integration packages
6. Creates the pages directory for your Inertia pages
## Usage
```bash
mix inertia.install
```
## Options
--client-framework FRAMEWORK Framework to use for the client-side integration
(react, vue, or svelte). Default is react.
--camelize-props Enable camelCase for props
--history-encrypt Enable history encryption
--typescript Enable TypeScript
--yes Don't prompt for confirmations
"""
end
end
if Code.ensure_loaded?(Igniter) do
defmodule Mix.Tasks.Inertia.Install do
@shortdoc __MODULE__.Docs.short_doc()
@moduledoc __MODULE__.Docs.long_doc()
use Igniter.Mix.Task
alias Igniter.Code.Common
alias Igniter.Code.Function
alias Igniter.Libs.Phoenix
alias Igniter.Project.Application
alias Igniter.Project.Config
alias Igniter.Project.Module
require Common
@impl Igniter.Mix.Task
def info(_argv, _parent) do
%Igniter.Mix.Task.Info{
schema: [
client_framework: :string,
camelize_props: :boolean,
history_encrypt: :boolean,
typescript: :boolean,
yes: :boolean
],
example: __MODULE__.Docs.example(),
defaults: [client_framework: "react"],
positional: []
}
end
@impl Igniter.Mix.Task
def igniter(igniter) do
igniter
|> setup_controller_helpers()
|> setup_html_helpers()
|> setup_router()
|> add_inertia_config()
|> update_root_layout()
|> update_esbuild_config()
|> setup_client()
|> create_pages_directory()
|> print_next_steps()
end
@doc false
def setup_controller_helpers(igniter) do
update_web_ex_helper(igniter, :controller, fn zipper ->
import_code = "import Inertia.Controller"
with {:ok, zipper} <- move_to_last_import(zipper) do
{:ok, Common.add_code(zipper, import_code)}
end
end)
end
@doc false
def setup_html_helpers(igniter) do
update_web_ex_helper(igniter, :html, fn zipper ->
import_code = """
import Inertia.HTML
"""
with {:ok, zipper} <- move_to_last_import(zipper) do
{:ok, Common.add_code(zipper, import_code)}
end
end)
end
# Run an update function within the quote do ... end block inside a *web.ex helper function
# update_fun must return {:ok, zipper} or :error.
defp update_web_ex_helper(igniter, helper_name, update_fun) do
web_module = Phoenix.web_module(igniter)
Module.find_and_update_module!(igniter, web_module, fn zipper ->
with {:ok, zipper} <- Function.move_to_def(zipper, helper_name, 0),
{:ok, zipper} <- Common.move_to_do_block(zipper) do
Common.within(zipper, update_fun)
end
end)
end
defp move_to_last_import(zipper) do
Common.move_to_last(zipper, &Function.function_call?(&1, :import))
end
@doc false
def setup_router(igniter) do
Phoenix.append_to_pipeline(igniter, :browser, "plug Inertia.Plug")
end
@doc false
def add_inertia_config(igniter) do
# Get endpoint module name based on app name
{igniter, endpoint_module} = Phoenix.select_endpoint(igniter)
# Determine configuration based on options
camelize_props = igniter.args.options[:camelize_props] || false
history_encryption = igniter.args.options[:history_encrypt] || false
config_options = [
endpoint: endpoint_module
]
# Add camelize_props config if specified
config_options =
if camelize_props do
Keyword.put(config_options, :camelize_props, true)
else
config_options
end
# Add history encryption config if specified
config_options =
if history_encryption do
Keyword.put(config_options, :history, encrypt: true)
else
config_options
end
# Add the configuration to config.exs
Enum.reduce(config_options, igniter, fn {key, value}, igniter ->
Config.configure(igniter, "config.exs", :inertia, [key], value)
end)
end
@doc false
def update_root_layout(igniter) do
file_path =
Path.join([
"lib",
web_dir(igniter),
"components",
"layouts",
"root.html.heex"
])
content = inertia_root_html()
Igniter.create_new_file(igniter, file_path, content, on_exists: :overwrite)
end
defp web_dir(igniter) do
igniter
|> Phoenix.web_module()
|> inspect()
|> Macro.underscore()
end
defp inertia_root_html do
"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="csrf-token" content={get_csrf_token()} />
<.inertia_title><%= assigns[:page_title] %></.inertia_title>
<.inertia_head content={@inertia_head} />
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
<script type="module" defer phx-track-static src={~p"/assets/app.js"} />
</head>
<body>
{@inner_content}
</body>
</html>
"""
end
@doc false
def update_esbuild_config(igniter) do
igniter
|> Config.configure("config.exs", :esbuild, [:version], "0.21.5")
|> Config.configure(
"config.exs",
:esbuild,
[Application.app_name(igniter)],
{:code,
Sourceror.parse_string!("""
[
args:
~w(js/app.jsx --bundle --chunk-names=chunks/[name]-[hash] --splitting --format=esm --target=es2020 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]
""")}
)
|> Igniter.add_task("esbuild.install")
end
@doc false
def setup_client(igniter) do
case igniter.args.options[:client_framework] do
"react" ->
igniter
|> install_client_package()
|> maybe_create_typescript_config()
|> Igniter.create_new_file("assets/js/app.jsx", inertia_app_jsx(),
on_exists: :overwrite
)
framework when framework in ["vue", "svelte"] ->
igniter
|> install_client_package()
|> maybe_create_typescript_config()
_ ->
igniter
end
end
@doc false
def create_pages_directory(igniter) do
Igniter.create_new_file(igniter, "assets/js/pages/.gitkeep", "", on_exists: :skip)
end
defp maybe_create_typescript_config(igniter) do
if igniter.args.options[:typescript] do
Igniter.create_new_file(igniter, "assets/tsconfig.json", react_tsconfig_json(),
on_exists: :overwrite
)
else
igniter
end
end
defp install_client_package(igniter) do
typescript = igniter.args.options[:typescript] || false
client_framework = igniter.args.options[:client_framework]
igniter
|> install_client_main_packages(client_framework)
|> maybe_install_typescript_deps(client_framework, typescript)
end
defp install_client_main_packages(igniter, "react") do
Igniter.add_task(igniter, "cmd", [
"npm install --prefix assets @inertiajs/react react react-dom"
])
end
defp install_client_main_packages(igniter, "vue") do
Igniter.add_task(igniter, "cmd", [
"npm install --prefix assets @inertiajs/vue3 vue vue-loader"
])
end
defp install_client_main_packages(igniter, "svelte") do
Igniter.add_task(igniter, "cmd", ["npm install --prefix assets @inertiajs/svelte svelte"])
end
defp maybe_install_typescript_deps(igniter, _, false), do: igniter
defp maybe_install_typescript_deps(igniter, "react", true) do
Igniter.add_task(igniter, "cmd", ["npm install --prefix assets --save-dev @types/react"])
end
defp maybe_install_typescript_deps(igniter, "vue", true) do
Igniter.add_task(igniter, "cmd", [
"npm install --prefix assets --save-dev @vue/compiler-sfc vue-tsc typescript"
])
end
defp maybe_install_typescript_deps(igniter, "svelte", true) do
Igniter.add_task(igniter, "cmd", [
"npm install --prefix assets --save-dev svelte-loader svelte-preprocess typescript"
])
end
defp react_tsconfig_json do
"""
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"@/*": ["./js/*"]
}
},
"include": ["js/**/*.ts", "js/**/*.tsx", "js/**/*.js", "js/**/*.jsx"],
"exclude": ["node_modules"]
}
"""
end
defp inertia_app_jsx do
"""
import React from "react";
import axios from "axios";
import { createInertiaApp } from "@inertiajs/react";
import { createRoot } from "react-dom/client";
axios.defaults.xsrfHeaderName = "x-csrf-token";
createInertiaApp({
resolve: async (name) => {
return await import(`./pages/${name}.jsx`);
},
setup({ App, el, props }) {
createRoot(el).render(<App {...props} />);
},
});
"""
end
defp print_next_steps(igniter) do
client_framework = igniter.args.options[:client_framework]
next_steps = []
next_steps =
if client_framework do
client_setup_steps = """
To finish setting up the client side integration:
1. Create your Inertia pages in the assets/js/pages directory
2. Set up your entry point file to initialize Inertia and default layout
"""
next_steps ++ [client_setup_steps]
else
next_steps
end
next_steps =
next_steps ++
[
"""
For more information on using Inertia with Phoenix, refer to:
https://hexdocs.pm/inertia/readme.html
"""
]
# Add completion notice to the Igniter
Enum.reduce(next_steps, igniter, fn step, igniter -> Igniter.add_notice(igniter, step) end)
end
end
else
defmodule Mix.Tasks.Inertia.Install do
@shortdoc "Install `igniter` in order to install Inertia."
@moduledoc __MODULE__.Docs.long_doc()
use Mix.Task
def run(_argv) do
Mix.shell().error("""
The task 'inertia.install' requires igniter. Please install igniter and try again.
For more information, see: https://hexdocs.pm/igniter/readme.html#installation
""")
exit({:shutdown, 1})
end
end
end