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, //, "<title inertia>" + 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, /<title>/, "<title inertia>" + end + else + say_error "Could not find the application layout file. Please add the following tags manually:", :red + say_error "- <title>..." + 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