require "yaml"
require "rails/generators"
require "rails/generators/base"
require_relative "helpers"
module Inertia
module Generators
class InstallGenerator < Rails::Generators::Base
include Helpers
FRAMEWORKS = YAML.load_file(File.expand_path("./frameworks.yml", __dir__))
source_root File.expand_path("./templates", __dir__)
class_option :framework, type: :string,
desc: "The framework you want to use with Inertia",
enum: FRAMEWORKS.keys,
default: nil
class_option :inertia_version, type: :string, default: "latest",
desc: "The version of Inertia.js to install"
class_option :typescript, type: :boolean, default: false,
desc: "Whether to use TypeScript"
class_option :package_manager, type: :string, default: nil, enum: %w[npm yarn bun pnpm],
desc: "The package manager you want to use to install Inertia's npm packages"
class_option :interactive, type: :boolean, default: true,
desc: "Whether to prompt for optional installations"
class_option :install_tailwind, type: :boolean, default: false,
desc: "Whether to install Tailwind CSS"
class_option :install_vite, type: :boolean, default: false,
desc: "Whether to install Vite Ruby"
class_option :example_page, type: :boolean, default: true,
desc: "Whether to add an example Inertia page"
class_option :verbose, type: :boolean, default: false,
desc: "Run the generator in verbose mode"
remove_class_option :skip_namespace, :skip_collision_check
def install
say "Installing Inertia's Rails adapter"
if inertia_resolved_version.version == "0"
say_error "Could not find the Inertia.js package version #{options[:inertia_version]}.", :red
exit(false)
end
install_vite unless ruby_vite_installed?
install_typescript if typescript?
install_tailwind if install_tailwind?
install_inertia
install_example_page if options[:example_page]
say "Copying bin/dev"
copy_file "#{__dir__}/templates/dev", "bin/dev"
chmod "bin/dev", 0o755, verbose: verbose?
say "Inertia's Rails adapter successfully installed", :green
end
private
def install_inertia
say "Adding Inertia's Rails adapter initializer"
template "initializer.rb", file_path("config/initializers/inertia_rails.rb")
say "Installing Inertia npm packages"
add_packages(*FRAMEWORKS[framework]["packages"])
add_packages(inertia_package)
unless File.read(vite_config_path).include?(FRAMEWORKS[framework]["vite_plugin_import"])
say "Adding Vite plugin for #{framework}"
insert_into_file vite_config_path, "\n #{FRAMEWORKS[framework]["vite_plugin_call"]},", after: "plugins: ["
prepend_file vite_config_path, "#{FRAMEWORKS[framework]["vite_plugin_import"]}\n"
end
say "Copying #{inertia_entrypoint} entrypoint"
template "#{framework}/#{inertia_entrypoint}", js_file_path("entrypoints/#{inertia_entrypoint}")
if application_layout.exist?
say "Adding #{inertia_entrypoint} script tag to the application layout"
headers = <<-ERB
<%= #{vite_tag} "inertia" %>
<%= inertia_headers %>
ERB
insert_into_file application_layout.to_s, headers, after: "<%= vite_client_tag %>\n"
if framework == "react" && !application_layout.read.include?("vite_react_refresh_tag")
say "Adding Vite React Refresh tag to the application layout"
insert_into_file application_layout.to_s, "<%= vite_react_refresh_tag %>\n ", before: "<%= vite_client_tag %>"
end
gsub_file application_layout.to_s, /
/, "" if framework != "svelte"
else
say_error "Could not find the application layout file. Please add the following tags manually:", :red
say_error "- ..."
say_error "+ ..."
say_error "+ <%= inertia_headers %>"
say_error "+ <%= vite_react_refresh_tag %>" if framework == "react"
say_error "+ <%= #{vite_tag} \"inertia\" %>"
end
end
def install_typescript
say "Adding TypeScript support"
if framework == "svelte" && inertia_resolved_version.release < Gem::Version.new("1.3.0")
say "WARNING: @inertiajs/svelte < 1.3.0 does not support TypeScript (resolved version: #{inertia_resolved_version}).", :yellow
say "Skipping TypeScript support for @inertiajs/svelte", :yellow
@typescript = false
return
end
add_packages(*FRAMEWORKS[framework]["packages_ts"])
end
def install_example_page
say "Copying example Inertia controller"
template "controller.rb", file_path("app/controllers/inertia_example_controller.rb")
say "Adding a route for the example Inertia controller"
route "get 'inertia-example', to: 'inertia_example#index'"
say "Copying page assets"
copy_files = FRAMEWORKS[framework]["copy_files"].merge(FRAMEWORKS[framework]["copy_files_#{typescript? ? "ts" : "js"}"])
copy_files.each do |source, destination|
template "#{framework}/#{source}", file_path(destination % {js_destination_path: js_destination_path})
end
end
def install_tailwind
say "Installing Tailwind CSS"
add_packages(%w[tailwindcss postcss autoprefixer @tailwindcss/forms @tailwindcss/typography @tailwindcss/container-queries])
template "tailwind/tailwind.config.js", file_path("tailwind.config.js")
copy_file "tailwind/postcss.config.js", file_path("postcss.config.js")
copy_file "tailwind/application.css", js_file_path("entrypoints/application.css")
if application_layout.exist?
say "Adding Tailwind CSS to the application layout"
insert_into_file application_layout.to_s, "<%= vite_stylesheet_tag \"application\" %>\n ", before: "<%= vite_client_tag %>"
else
say_error "Could not find the application layout file. Please add the following tags manually:", :red
say_error "+ <%= vite_stylesheet_tag \"application\" %>" if install_tailwind?
end
end
def install_vite
unless install_vite?
say_error "This generator only supports Ruby on Rails with Vite.", :red
exit(false)
end
in_root do
Bundler.with_original_env do
if (capture = run("bundle add vite_rails", capture: !verbose?))
say "Vite Rails gem successfully installed", :green
else
say capture
say_error "Failed to install Vite Rails gem", :red
exit(false)
end
if (capture = run("bundle exec vite install", capture: !verbose?))
say "Vite Rails successfully installed", :green
else
say capture
say_error "Failed to install Vite Rails", :red
exit(false)
end
end
end
end
def ruby_vite_installed?
return true if package_manager && ruby_vite?
if package_manager.nil?
say_status "Could not find a package.json file to install Inertia to.", nil
elsif gem_installed?("webpacker") || gem_installed?("shakapacker")
say "Webpacker or Shakapacker is installed.", :yellow
say "Vite Ruby can work alongside Webpacker or Shakapacker, but it might cause issues.", :yellow
say "Please see the Vite Ruby documentation for the migration guide:", :yellow
say "https://vite-ruby.netlify.app/guide/migration.html#webpacker-%F0%9F%93%A6", :yellow
else
say_status "Could not find a Vite configuration files (`config/vite.json` & `vite.config.{ts,js,mjs,cjs,mts,cts}`).", nil
end
false
end
def gem_installed?(name)
regex = /^[^#]*gem\s+['"]#{name}['"]/
File.read(file_path("Gemfile")).match?(regex)
end
def application_layout
@application_layout ||= Pathname.new(file_path("app/views/layouts/application.html.erb"))
end
def ruby_vite?
file?("config/vite.json") && vite_config_path
end
def package_manager
options[:package_manager] || detect_package_manager
end
def add_packages(*packages)
in_root do
run "#{package_manager} add #{packages.join(" ")} #{verbose? ? "" : "--silent"}"
end
end
def detect_package_manager
return nil unless file?("package.json")
if file?("package-lock.json")
"npm"
elsif file?("bun.lockb")
"bun"
elsif file?("pnpm-lock.yaml")
"pnpm"
else
"yarn"
end
end
def vite_config_path
@vite_config_path ||= Dir.glob(file_path("vite.config.{ts,js,mjs,cjs,mts,cts}")).first
end
def install_vite?
return @install_vite if defined?(@install_vite)
@install_vite = options[:install_vite] || yes?("Would you like to install Vite Ruby? (y/n)", :green)
end
def install_tailwind?
return @install_tailwind if defined?(@install_tailwind)
@install_tailwind = options[:install_tailwind] || yes?("Would you like to install Tailwind CSS? (y/n)", :green)
end
def typescript?
return @typescript if defined?(@typescript)
@typescript = options[:typescript] || yes?("Would you like to use TypeScript? (y/n)", :green)
end
def inertia_entrypoint
"inertia.#{typescript? ? "ts" : "js"}"
end
def vite_tag
typescript? ? "vite_typescript_tag" : "vite_javascript_tag"
end
def inertia_resolved_version
@inertia_resolved_version ||= Gem::Version.new(`npm show @inertiajs/core@#{options[:inertia_version]} version`.strip)
end
def verbose?
options[:verbose]
end
def inertia_package
"#{FRAMEWORKS[framework]["inertia_package"]}@#{options[:inertia_version]}"
end
def framework
@framework ||= options[:framework] || ask("What framework do you want to use with Inertia?", :green, limited_to: FRAMEWORKS.keys, default: "react")
end
end
end
end