# frozen_string_literal: true
require 'yaml'
require 'rails/generators'
require 'rails/generators/base'
require_relative 'helpers'
require_relative 'js_package_manager'
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: JSPackageManager.package_managers,
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 :tailwind, type: :boolean, default: false,
desc: 'Whether to install Tailwind CSS'
class_option :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_dependencies(*FRAMEWORKS[framework]['packages'])
add_dependencies(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_ssr_head %>
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, /
/, '' unless 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_ssr_head %>'
say_error '+ <%= vite_react_refresh_tag %>' if framework == 'react'
say_error "+ <%= #{vite_tag} \"inertia\" %>"
end
end
def install_typescript
say 'Adding TypeScript support'
if 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_dependencies(*FRAMEWORKS[framework]['packages_ts'])
say 'Copying adding scripts to package.json'
run 'npm pkg set scripts.check="svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json"' if svelte?
run 'npm pkg set scripts.check="vue-tsc -p tsconfig.app.json && tsc -p tsconfig.node.json"' if framework == 'vue'
run 'npm pkg set scripts.check="tsc -p tsconfig.app.json && tsc -p tsconfig.node.json"' if framework == 'react'
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(format(destination, js_destination_path: js_destination_path))
end
end
def install_tailwind
say 'Installing Tailwind CSS'
add_dependencies(%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.present? && ruby_vite?
if !package_manager.present?
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
@package_manager ||= JSPackageManager.new(self)
end
def add_dependencies(*packages)
package_manager.add_dependencies(*packages)
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[: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[: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 svelte?
framework.start_with? 'svelte'
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