diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 6d57e77..646c2d9 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -8,20 +8,53 @@ on:
pull_request:
jobs:
+ lint:
+ runs-on: ubuntu-latest
+ name: Linter
+ env:
+ BUNDLE_JOBS: 4
+ BUNDLE_RETRY: 3
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: "3.3"
+ bundler: latest
+ bundler-cache: true
+ - name: Run StandardRB
+ run: bundle exec standardrb
+
build:
runs-on: ubuntu-latest
name: Ruby ${{ matrix.ruby }}
+ env:
+ BUNDLE_JOBS: 4
+ BUNDLE_RETRY: 3
strategy:
matrix:
ruby:
- - '3.2.3'
+ - "3.3"
+ - "3.2"
+ - "3.1"
+ - "3.0"
steps:
- - uses: actions/checkout@v4
- - name: Set up Ruby
- uses: ruby/setup-ruby@v1
- with:
- ruby-version: ${{ matrix.ruby }}
- bundler-cache: true
- - name: Run the default task
- run: bundle exec rake
+ - uses: actions/checkout@v4
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ matrix.ruby }}
+ bundler-cache: true
+ bundler: latest
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+ - name: Set Node.js 20.x
+ uses: actions/setup-node@v3
+ with:
+ node-version: 20.x
+ - name: Install yarn
+ run: npm install -g yarn
+ - name: Run tests
+ run: bundle exec rspec
diff --git a/Gemfile b/Gemfile
index 3eadde4..39b09a9 100644
--- a/Gemfile
+++ b/Gemfile
@@ -10,3 +10,5 @@ gem "rake", "~> 13.0"
gem "rspec", "~> 3.0"
gem "standard", "~> 1.3"
+
+gem "generator_spec", "~> 0.10"
diff --git a/lib/generators/inertia/install/frameworks.yml b/lib/generators/inertia/install/frameworks.yml
new file mode 100644
index 0000000..1007fa7
--- /dev/null
+++ b/lib/generators/inertia/install/frameworks.yml
@@ -0,0 +1,41 @@
+react:
+ packages:
+ - "@inertiajs/react"
+ - "react"
+ - "react-dom"
+ - "@vitejs/plugin-react"
+ vite_plugin_import: "import react from '@vitejs/plugin-react'"
+ vite_plugin_call: "react()"
+ copy_files:
+ "InertiaExample.jsx": "%{js_destination_path}/pages/InertiaExample.jsx"
+ "InertiaExample.module.css": "%{js_destination_path}/pages/InertiaExample.module.css"
+ "../assets/react.svg": "%{js_destination_path}/assets/react.svg"
+ "../assets/inertia.svg": "%{js_destination_path}/assets/inertia.svg"
+ "../assets/vite_ruby.svg": "%{js_destination_path}/assets/vite_ruby.svg"
+
+vue:
+ packages:
+ - "@inertiajs/vue3"
+ - "vue"
+ - "@vitejs/plugin-vue"
+ vite_plugin_import: "import vue from '@vitejs/plugin-vue'"
+ vite_plugin_call: "vue()"
+ copy_files:
+ "InertiaExample.vue": "%{js_destination_path}/pages/InertiaExample.vue"
+ "../assets/vue.svg": "%{js_destination_path}/assets/vue.svg"
+ "../assets/inertia.svg": "%{js_destination_path}/assets/inertia.svg"
+ "../assets/vite_ruby.svg": "%{js_destination_path}/assets/vite_ruby.svg"
+
+svelte:
+ packages:
+ - "@inertiajs/svelte"
+ - "svelte"
+ - "@sveltejs/vite-plugin-svelte"
+ vite_plugin_import: "import { svelte } from '@sveltejs/vite-plugin-svelte'"
+ vite_plugin_call: "svelte()"
+ copy_files:
+ "svelte.config.js": "svelte.config.js"
+ "InertiaExample.svelte": "%{js_destination_path}/pages/InertiaExample.svelte"
+ "../assets/svelte.svg": "%{js_destination_path}/assets/svelte.svg"
+ "../assets/inertia.svg": "%{js_destination_path}/assets/inertia.svg"
+ "../assets/vite_ruby.svg": "%{js_destination_path}/assets/vite_ruby.svg"
diff --git a/lib/generators/inertia/install/helpers.rb b/lib/generators/inertia/install/helpers.rb
new file mode 100644
index 0000000..014109d
--- /dev/null
+++ b/lib/generators/inertia/install/helpers.rb
@@ -0,0 +1,42 @@
+module Inertia
+ module Generators
+ module Helpers
+ ### FS Helpers
+ def js_destination_path
+ defined?(ViteRuby) ? ViteRuby.config.source_code_dir : "app/frontend"
+ end
+
+ def js_destination_root
+ file_path(js_destination_path)
+ end
+
+ def js_file_path(*relative_path)
+ File.join(js_destination_root, *relative_path)
+ end
+
+ def file?(*relative_path)
+ File.file?(file_path(*relative_path))
+ end
+
+ def file_path(*relative_path)
+ File.join(destination_root, *relative_path)
+ end
+
+ # Interactivity Helpers
+ def ask(*)
+ unless options[:interactive]
+ say_error "Specify all options when running the generator non-interactively.", :red
+ exit(1)
+ end
+
+ super
+ end
+
+ def yes?(*)
+ return false unless options[:interactive]
+
+ super
+ end
+ end
+ end
+end
diff --git a/lib/generators/inertia/install/install_generator.rb b/lib/generators/inertia/install/install_generator.rb
index 58ce21c..9127a91 100644
--- a/lib/generators/inertia/install/install_generator.rb
+++ b/lib/generators/inertia/install/install_generator.rb
@@ -1,66 +1,71 @@
+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__)
- APPLICATION_LAYOUT = Rails.root.join("app/views/layouts/application.html.erb")
-
- FRAMEWORKS = {
- "react" => {
- packages: %w[@inertiajs/react react react-dom],
- dev_packages: %w[@vitejs/plugin-react],
- vite_plugin_import: "import react from '@vitejs/plugin-react'",
- vite_plugin_call: "react()",
- copy_files: {
- "InertiaExample.jsx" => "#{root_path}/pages/InertiaExample.jsx",
- "InertiaExample.module.css" => "#{root_path}/pages/InertiaExample.module.css",
- "../assets/react.svg" => "#{root_path}/assets/react.svg",
- "../assets/inertia.svg" => "#{root_path}/assets/inertia.svg",
- "../assets/vite_ruby.svg" => "#{root_path}/assets/vite_ruby.svg"
- }
- },
- "vue" => {
- packages: %w[@inertiajs/vue3 vue],
- dev_packages: %w[@vitejs/plugin-vue],
- vite_plugin_import: "import vue from '@vitejs/plugin-vue'",
- vite_plugin_call: "vue()",
- copy_files: {
- "InertiaExample.vue" => "#{root_path}/pages/InertiaExample.vue",
- "../assets/vue.svg" => "#{root_path}/assets/vue.svg",
- "../assets/inertia.svg" => "#{root_path}/assets/inertia.svg",
- "../assets/vite_ruby.svg" => "#{root_path}/assets/vite_ruby.svg"
- }
- },
- "svelte" => {
- packages: %w[@inertiajs/svelte svelte @sveltejs/vite-plugin-svelte],
- dev_packages: %w[@vitejs/plugin-vue],
- vite_plugin_import: "import { svelte } from '@sveltejs/vite-plugin-svelte'",
- vite_plugin_call: "svelte()",
- copy_files: {
- "svelte.config.js" => "svelte.config.js",
- "InertiaExample.svelte" => "#{root_path}/pages/InertiaExample.svelte",
- "../assets/svelte.svg" => "#{root_path}/assets/svelte.svg",
- "../assets/inertia.svg" => "#{root_path}/assets/inertia.svg",
- "../assets/vite_ruby.svg" => "#{root_path}/assets/vite_ruby.svg"
- }
- }
- }
+ 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],
+ 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"
+
+ remove_class_option :skip_namespace, :skip_collision_check
def install
say "Installing Inertia's Rails adapter"
- if package_manager.nil?
- say "Could not find a package.json file to install Inertia to.", :red
- exit!
+ if package_manager.nil? || !ruby_vite?
+ if package_manager.nil?
+ say_status "Could not find a package.json file to install Inertia to.", nil
+ else
+ say_status "Could not find a Vite configuration files (`config/vite.json` & `vite.config.{ts,js,mjs,cjs}`).", nil
+ end
+
+ if install_vite?
+ in_root do
+ Bundler.with_original_env do
+ run "bundle add vite_rails"
+ run "bundle exec vite install"
+ end
+ end
+ else
+ say_error "This generator only supports Ruby on Rails with Vite.", :red
+ exit(false)
+ end
end
- unless ruby_vite?
- say "Could not find a Vite configuration file `config/vite.json`. This generator only supports Ruby on Rails with Vite.", :red
- exit!
- end
+ 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: false
+
say "Inertia's Rails adapter successfully installed", :green
end
@@ -68,72 +73,99 @@ def install
def install_inertia
say "Adding Inertia's Rails adapter initializer"
- template "initializer.rb", Rails.root.join("config/initializers/inertia_rails.rb").to_s
+ template "initializer.rb", file_path("config/initializers/inertia_rails.rb")
say "Installing Inertia npm packages"
- add_packages(*FRAMEWORKS[framework][:packages])
- add_packages("--save-dev", *FRAMEWORKS[framework][:dev_packages])
+ add_packages(*FRAMEWORKS[framework]["packages"])
- unless File.read(vite_config_path).include?(FRAMEWORKS[framework][:vite_plugin_import])
+ 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
-
- unless Rails.root.join("package.json").read.include?('"type": "module"')
- say 'Add "type": "module", to the package.json file'
- gsub_file Rails.root.join("package.json").to_s, /\A\s*\{/, "{\n \"type\": \"module\","
+ 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 into Vite entrypoints", :blue
- template "#{framework}/inertia.js", Rails.root.join("#{root_path}/entrypoints/inertia.js").to_s
-
- say "Adding inertia.js script tag to the application layout"
- headers = <<-ERB
- <%= vite_javascript_tag 'inertia' %>
+ 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
- insert_into_file APPLICATION_LAYOUT.to_s, headers, after: "<%= vite_client_tag %>\n"
+ 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, /
/, ""
+ 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", Rails.root.join("app/controllers/inertia_example_controller.rb").to_s
+ 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 framework related files"
- FRAMEWORKS[framework][:copy_files].each do |source, destination|
- template "#{framework}/#{source}", Rails.root.join(destination).to_s
+ 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 application_layout
+ @application_layout ||= Pathname.new(file_path("app/views/layouts/application.html.erb"))
+ end
+
def ruby_vite?
- Rails.root.join("config/vite.json").exist? && vite_config_path
+ file?("config/vite.json") && vite_config_path
end
def package_manager
- return @package_manager if defined?(@package_manager)
-
- @package_manager = detect_package_manager
+ options[:package_manager] || detect_package_manager
end
def add_packages(*packages)
- run "#{package_manager} add #{packages.join(" ")}"
+ in_root do
+ run "#{package_manager} add #{packages.join(" ")} --silent"
+ end
end
def detect_package_manager
- return nil unless Rails.root.join("package.json").exist?
+ return nil unless file?("package.json")
- if Rails.root.join("package-lock.json").exist?
+ if file?("package-lock.json")
"npm"
- elsif Rails.root.join("bun.config.js").exist?
+ elsif file?("bun.config.js") || file?("bun.lockb")
"bun"
else
"yarn"
@@ -141,15 +173,23 @@ def detect_package_manager
end
def vite_config_path
- @vite_config_path ||= Dir.glob(Rails.root.join("vite.config.{ts,js,mjs,cjs}")).first
+ @vite_config_path ||= Dir.glob(file_path("vite.config.{ts,js,mjs,cjs}")).first
end
- def framework
- @framework ||= ask("What framework do you want to use with Inertia?", limited_to: FRAMEWORKS.keys, default: "react")
+ 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 root_path
- (defined?(ViteRuby) ? ViteRuby.config.source_code_dir : "app/frontend")
+ 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 framework
+ @framework ||= options[:framework] || ask("What framework do you want to use with Inertia?", :green, limited_to: FRAMEWORKS.keys, default: "react")
end
end
end
diff --git a/lib/generators/inertia/install/templates/dev b/lib/generators/inertia/install/templates/dev
new file mode 100644
index 0000000..ef33f02
--- /dev/null
+++ b/lib/generators/inertia/install/templates/dev
@@ -0,0 +1,23 @@
+#!/usr/bin/env sh
+
+export PORT="${PORT:-3000}"
+
+if command -v overmind 1> /dev/null 2>&1
+then
+ overmind start -f Procfile.dev "$@"
+ exit $?
+fi
+
+if command -v hivemind 1> /dev/null 2>&1
+then
+ echo "Hivemind is installed. Running the application with Hivemind..."
+ exec hivemind Procfile.dev "$@"
+ exit $?
+fi
+
+if gem list --no-installed --exact --silent foreman; then
+ echo "Installing foreman..."
+ gem install foreman
+fi
+
+foreman start -f Procfile.dev "$@"
diff --git a/lib/generators/inertia/install/templates/tailwind/application.css b/lib/generators/inertia/install/templates/tailwind/application.css
new file mode 100644
index 0000000..8666d2f
--- /dev/null
+++ b/lib/generators/inertia/install/templates/tailwind/application.css
@@ -0,0 +1,13 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/*
+
+@layer components {
+ .btn-primary {
+ @apply py-2 px-4 bg-blue-200;
+ }
+}
+
+*/
diff --git a/lib/generators/inertia/install/templates/tailwind/postcss.config.js b/lib/generators/inertia/install/templates/tailwind/postcss.config.js
new file mode 100644
index 0000000..2e7af2b
--- /dev/null
+++ b/lib/generators/inertia/install/templates/tailwind/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/lib/generators/inertia/install/templates/tailwind/tailwind.config.js.tt b/lib/generators/inertia/install/templates/tailwind/tailwind.config.js.tt
new file mode 100644
index 0000000..dae9371
--- /dev/null
+++ b/lib/generators/inertia/install/templates/tailwind/tailwind.config.js.tt
@@ -0,0 +1,18 @@
+/** @type {import('tailwindcss').Config} */
+
+module.exports = {
+ content: [
+ './public/*.html',
+ './app/helpers/**/*.rb',
+ './<%= js_destination_path %>/**/*.{js,ts,jsx,tsx,vue,svelte}',
+ './app/views/**/*.{erb,haml,html,slim}'
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [
+ require('@tailwindcss/forms'),
+ require('@tailwindcss/typography'),
+ require('@tailwindcss/container-queries'),
+ ]
+}
diff --git a/lib/generators/inertia/scaffold_controller/templates/controller.rb.tt b/lib/generators/inertia/scaffold_controller/templates/controller.rb.tt
index 78fee1e..03e9a49 100644
--- a/lib/generators/inertia/scaffold_controller/templates/controller.rb.tt
+++ b/lib/generators/inertia/scaffold_controller/templates/controller.rb.tt
@@ -84,17 +84,16 @@ class <%= controller_class_name %>Controller < ApplicationController
def <%= "serialize_#{singular_table_name}" %>(<%= singular_table_name %>)
<%= singular_table_name %>.as_json(only: [
<%= attributes_to_serialize.map { |attribute| ":#{attribute}" }.join(", ") %>
- ])<% if attributes.any?(&:attachment?) -%>.tap do |hash|
-<% attributes.filter(&:attachment?).map do |attribute| -%>
+ ])<%- if attributes.any?(&:attachment?) || attributes.any?(&:attachments?) -%>.tap do |hash|
+ <%- attributes.filter(&:attachment?).map do |attribute| -%>
hash["<%= attribute.column_name %>"] = {filename: <%= singular_table_name %>.<%= attribute.column_name %>.filename, url: url_for(<%= singular_table_name %>.<%= attribute.column_name %>)} if <%= singular_table_name %>.<%= attribute.column_name %>.attached?
-<% end -%>
- end<% end -%><% if attributes.any?(&:attachments?) -%>.tap do |hash|
-<% attributes.filter(&:attachments?).map do |attribute| -%>
+ <%- end -%>
+ <%- attributes.filter(&:attachments?).map do |attribute| -%>
hash["<%= attribute.column_name %>"] =
<%= singular_table_name %>.<%= attribute.column_name %>.flat_map do |file|
{filename: file.filename.to_s, url: url_for(file)}
end
-<% end -%>
+ <%- end -%>
end<% end %>
end
end
diff --git a/lib/inertia_rails_contrib/generators_helper.rb b/lib/inertia_rails_contrib/generators_helper.rb
index 7435b1f..5f3d9dd 100644
--- a/lib/inertia_rails_contrib/generators_helper.rb
+++ b/lib/inertia_rails_contrib/generators_helper.rb
@@ -9,7 +9,7 @@ def self.guess_the_default_framework
when /@inertiajs\/vue3/
"vue"
else
- puts "Could not determine the Inertia.js framework you are using."
+ say_error "Could not determine the Inertia.js framework you are using."
end
end
diff --git a/spec/fixtures/dummy/Gemfile b/spec/fixtures/dummy/Gemfile
new file mode 100644
index 0000000..38da745
--- /dev/null
+++ b/spec/fixtures/dummy/Gemfile
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
diff --git a/spec/fixtures/dummy/app/views/layouts/application.html.erb b/spec/fixtures/dummy/app/views/layouts/application.html.erb
new file mode 100644
index 0000000..57ae714
--- /dev/null
+++ b/spec/fixtures/dummy/app/views/layouts/application.html.erb
@@ -0,0 +1,13 @@
+
+
+
+ TestContrib
+
+ <%= csrf_meta_tags %>
+ <%= csp_meta_tag %>
+
+
+
+<%= yield %>
+
+
diff --git a/spec/fixtures/dummy/config/routes.rb b/spec/fixtures/dummy/config/routes.rb
new file mode 100644
index 0000000..e69de29
diff --git a/spec/fixtures/with_vite/app/views/layouts/application.html.erb b/spec/fixtures/with_vite/app/views/layouts/application.html.erb
new file mode 100644
index 0000000..5dc926b
--- /dev/null
+++ b/spec/fixtures/with_vite/app/views/layouts/application.html.erb
@@ -0,0 +1,16 @@
+
+
+
+ TestContrib
+
+ <%= csrf_meta_tags %>
+ <%= csp_meta_tag %>
+
+ <%= vite_client_tag %>
+ <%= vite_javascript_tag 'application' %>
+
+
+
+<%= yield %>
+
+
diff --git a/spec/fixtures/with_vite/config/vite.json b/spec/fixtures/with_vite/config/vite.json
new file mode 100644
index 0000000..e69de29
diff --git a/spec/fixtures/with_vite/package.json b/spec/fixtures/with_vite/package.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/spec/fixtures/with_vite/package.json
@@ -0,0 +1 @@
+{}
diff --git a/spec/fixtures/with_vite/vite.config.ts b/spec/fixtures/with_vite/vite.config.ts
new file mode 100644
index 0000000..3129636
--- /dev/null
+++ b/spec/fixtures/with_vite/vite.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite'
+import RubyPlugin from 'vite-plugin-ruby'
+
+export default defineConfig({
+ plugins: [
+ RubyPlugin(),
+ ],
+})
diff --git a/spec/lib/generators/install/install_generator_spec.rb b/spec/lib/generators/install/install_generator_spec.rb
new file mode 100644
index 0000000..eacf095
--- /dev/null
+++ b/spec/lib/generators/install/install_generator_spec.rb
@@ -0,0 +1,243 @@
+require_relative "../../../../lib/generators/inertia/install/install_generator"
+require "generator_spec"
+
+RSpec.describe Inertia::Generators::InstallGenerator, type: :generator do
+ destination File.expand_path("../../../../../tmp", __FILE__)
+
+ let(:args) { %W[--framework=#{framework} --no-interactive -q] }
+ let(:framework) { :react }
+
+ subject(:generator) { run_generator(args) }
+
+ context "without vite" do
+ before do
+ prepare_application(with_vite: false)
+ end
+
+ it "exits with an error" do
+ expect { generator }.to raise_error(SystemExit)
+ end
+
+ context "with --install-vite" do
+ let(:args) { super() + %w[--install-vite] }
+
+ it "installs Vite" do
+ expect { generator }.not_to raise_error
+ expect_example_page_for(:react)
+ expect_packages_for(:react)
+ expect(destination_root).to(have_structure do
+ directory("app/frontend") do
+ no_file("entrypoints/application.css")
+ end
+ no_file("postcss.config.js")
+ no_file("tailwind.config.js")
+ end)
+ end
+ end
+ end
+
+ context "with --install-tailwind" do
+ let(:args) { super() + %w[--install-tailwind] }
+
+ before { prepare_application }
+
+ it "installs Tailwind" do
+ expect { generator }.not_to raise_error
+ expect_tailwind_config
+ end
+ end
+
+ context "with --framework=svelte" do
+ let(:framework) { :svelte }
+
+ before { prepare_application }
+
+ it "installs the Inertia adapter" do
+ expect { generator }.not_to raise_error
+
+ expect_example_page_for(framework)
+ expect_inertia_prepared_for(framework)
+ expect_packages_for(framework)
+ end
+ end
+
+ context "with --framework=vue" do
+ let(:framework) { :vue }
+
+ before { prepare_application }
+
+ it "installs the Inertia adapter" do
+ expect { generator }.not_to raise_error
+
+ expect_example_page_for(framework)
+ expect_inertia_prepared_for(framework)
+ expect_packages_for(framework)
+ end
+ end
+
+ context "with --framework=react" do
+ let(:framework) { :react }
+
+ before { prepare_application }
+
+ it "installs the Inertia adapter" do
+ expect { generator }.not_to raise_error
+
+ expect_example_page_for(framework)
+ expect_inertia_prepared_for(framework)
+ expect_packages_for(framework)
+ end
+ end
+
+ context "with yarn" do
+ before { prepare_application }
+
+ it "installs the Inertia adapter" do
+ expect { generator }.not_to raise_error
+
+ expect_example_page_for(:react)
+ expect_packages_for(:react)
+ end
+ end
+
+ context "with npm" do
+ let(:args) { super() + %w[--package-manager=npm] }
+
+ before { prepare_application }
+
+ it "installs the Inertia adapter" do
+ expect { generator }.not_to raise_error
+
+ expect_example_page_for(:react)
+ expect_packages_for(:react)
+ end
+ end
+
+ context "with bun" do
+ let(:args) { super() + %w[--package-manager=bun] }
+
+ before { prepare_application }
+
+ it "installs the Inertia adapter" do
+ expect { generator }.not_to raise_error
+
+ expect_example_page_for(:react)
+ expect_packages_for(:react)
+ end
+ end
+
+ def prepare_application(with_vite: true)
+ prepare_destination
+ FileUtils.cp_r(Dir["spec/fixtures/dummy/*"], destination_root)
+ FileUtils.cp_r(Dir["spec/fixtures/with_vite/*"], destination_root) if with_vite
+ end
+
+ def expect_tailwind_config
+ expect(destination_root).to(have_structure do
+ directory("app/frontend") do
+ file("entrypoints/application.css")
+ end
+ file("postcss.config.js")
+ file("tailwind.config.js")
+ end)
+ end
+
+ def expect_vite_config
+ expect(destination_root).to(have_structure do
+ directory("config") do
+ file("vite.json")
+ end
+ file("vite.config.js")
+ end)
+ end
+
+ def expect_packages_for(framework)
+ expect(destination_root).to(have_structure do
+ file("package.json") do
+ case framework
+ when :react
+ contains('"@inertiajs/react":')
+ contains('"react":')
+ contains('"react-dom":')
+ contains('"@vitejs/plugin-react":')
+ when :vue
+ contains('"@inertiajs/vue3":')
+ contains('"vue":')
+ contains('"@vitejs/plugin-vue":')
+ when :svelte
+ contains('"@inertiajs/svelte":')
+ contains('"svelte":')
+ contains('"@sveltejs/vite-plugin-svelte":')
+ end
+ end
+ end)
+ end
+
+ def expect_inertia_prepared_for(framework)
+ expect(destination_root).to(have_structure do
+ case framework
+ when :react
+ file("vite.config.ts") do
+ contains("react()")
+ end
+ file("app/frontend/entrypoints/inertia.js") do
+ contains("import { createInertiaApp } from '@inertiajs/react'")
+ end
+ when :vue
+ file("vite.config.ts") do
+ contains("vue()")
+ end
+ file("app/frontend/entrypoints/inertia.js") do
+ contains("import { createInertiaApp } from '@inertiajs/vue3'")
+ end
+ when :svelte
+ file("svelte.config.js") do
+ contains("preprocess: vitePreprocess()")
+ end
+ file("vite.config.ts") do
+ contains("svelte()")
+ end
+ file("app/frontend/entrypoints/inertia.js") do
+ contains("import { createInertiaApp } from '@inertiajs/svelte'")
+ end
+ end
+ file("app/views/layouts/application.html.erb") do
+ contains('<%= vite_javascript_tag "inertia" %>')
+ if framework == :react
+ contains("<%= vite_react_refresh_tag %>")
+ else
+ does_not_contain("<%= vite_react_refresh_tag %>")
+ end
+ end
+ file("config/initializers/inertia_rails.rb") do
+ contains("config.version = ViteRuby.digest")
+ end
+
+ file("bin/dev") do
+ contains("overmind start -f Procfile.dev")
+ end
+ end)
+ end
+
+ def expect_example_page_for(framework)
+ expect(destination_root).to(have_structure do
+ directory("app/frontend") do
+ case framework
+ when :react
+ file("pages/InertiaExample.jsx")
+ file("pages/InertiaExample.module.css")
+ file("assets/react.svg")
+ when :vue
+ file("pages/InertiaExample.vue")
+ file("assets/vue.svg")
+ when :svelte
+ file("pages/InertiaExample.svelte")
+ file("assets/svelte.svg")
+ end
+
+ file("assets/inertia.svg")
+ file("assets/vite_ruby.svg")
+ end
+ end)
+ end
+end