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 :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"
install_vite unless ruby_vite_installed?
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"])
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.js entrypoint"
template "#{framework}/inertia.js", js_file_path("entrypoints/inertia.js")
if application_layout.exist?
say "Adding inertia.js script tag to the application layout"
headers = <<-ERB
<%= vite_javascript_tag "inertia" %>
<%= inertia_headers %>
ERB
headers += "\n <%= vite_stylesheet_tag \"application\" %>" if install_tailwind?
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 %>"
gsub_file application_layout.to_s, /
/, ""
end
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_javascript_tag \"inertia\" %>"
end
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"
FRAMEWORKS[framework]["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 verbose?
options[:verbose]
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