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/CHANGELOG.md b/CHANGELOG.md index 8ad6b85..10d2a60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning]. ## [Unreleased] +Added: + +- Improve installation generator ([@skryukov]) + - option to install Vite Rails gem (`--install-vite`) + - option to install Tailwind CSS (`--install-tailwind`) + - option to install without interactivity (`--no-interaction` & `--framework=react|vue|svelte`) + - option to skip example page generation (`--no-example-page`) + - option to choose package manager (`--package-manager=yarn|npm|bun`) + - generate `bin/dev` + ## [0.1.1] - 2024-06-17 ### Fixed: 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/README.md b/README.md index b35ceb4..bb5ae76 100644 --- a/README.md +++ b/README.md @@ -20,40 +20,7 @@ If bundler is not being used to manage dependencies, install the gem by executin ### Installation generator -`InertiaRailsContrib` comes with a generator that installs and sets up Inertia in a Rails application. **It requires the [Vite Rails](https://vite-ruby.netlify.app/guide/rails.html) gem to be installed and configured in the application.** - -
-Creating a new Rails application and configuring Vite - -This is actually a simple process. First, create a new Rails application: -```bash -rails new myapp --skip-js -``` - -Next, install the Vite Rails gem: - -```bash -bundle add vite_rails -bundle exec vite install -``` - -If you use macOS, you may need to edit the `config/vite.rb` file to add the following line: - -```json -{ - "development": { -+ "host": "127.0.0.1", - "autoBuild": true, - "publicOutputDir": "vite-dev", - "port": 3036 - } -} -``` - -That's it! Vite is now installed and configured in the Rails application. For more information, refer to the [Vite Ruby documentation](https://vite-ruby.netlify.app) and the [Vite-lizing Rails: get live reload and hot replacement with Vite Ruby](https://evilmartians.com/chronicles/vite-lizing-rails-get-live-reload-and-hot-replacement-with-vite-ruby) article. - -The next step is to install Inertia! -
+`InertiaRailsContrib` comes with a generator that installs and sets up Inertia in a Rails application. It automatically detects if the [Vite Rails](https://vite-ruby.netlify.app/guide/rails.html) gem is installed and will attempt to install it if not present. To install and setup Inertia in a Rails application, execute the following command in the terminal: @@ -62,73 +29,70 @@ bundle add inertia_rails-contrib bin/rails generate inertia:install ``` -This command will ask you for the frontend framework you are using (React, Vue, or Svelte) and will install the necessary dependencies and set up the application to work with Inertia. +This command will: +- Check for Vite Rails and install it if not present +- Ask you to choose your preferred frontend framework (React, Vue, or Svelte) +- Ask if you want to install Tailwind CSS +- Install necessary dependencies +- Set up the application to work with Inertia +- Copy example Inertia controller and views (can be skipped with the `--skip-example` option) Example output: ```bash $ bin/rails generate inertia:install - Installing Inertia's Rails adapter +Could not find a package.json file to install Inertia to. +Would you like to install Vite Ruby? (y/n) y + run bundle add vite_rails from "." +Vite Rails gem successfully installed + run bundle exec vite install from "." +Vite Rails successfully installed +Would you like to install Tailwind CSS? (y/n) y +Installing Tailwind CSS + run npm add tailwindcss postcss autoprefixer @tailwindcss/forms @tailwindcss/typography @tailwindcss/container-queries --silent from "." + create tailwind.config.js + create postcss.config.js + create app/frontend/entrypoints/application.css +Adding Tailwind CSS to the application layout + insert app/views/layouts/application.html.erb Adding Inertia's Rails adapter initializer create config/initializers/inertia_rails.rb Installing Inertia npm packages What framework do you want to use with Inertia? [react, vue, svelte] (react) - run npm add @inertiajs/inertia @inertiajs/react react react-dom from "." - -added 6 packages, removed 42 packages, and audited 69 packages in 8s - -18 packages are looking for funding - run `npm fund` for details - -2 moderate severity vulnerabilities - -Some issues need review, and may require choosing -a different dependency. - -Run `npm audit` for details. - run npm add --save-dev @vitejs/plugin-react from "." - -added 58 packages, and audited 127 packages in 6s - -22 packages are looking for funding - run `npm fund` for details - -2 moderate severity vulnerabilities - -Some issues need review, and may require choosing -a different dependency. - -Run `npm audit` for details. + run npm add @inertiajs/react react react-dom @vitejs/plugin-react --silent from "." Adding Vite plugin for react insert vite.config.ts prepend vite.config.ts -Add "type": "module", to the package.json file - gsub package.json -Copying inertia.js into Vite entrypoints +Copying inertia.js entrypoint create app/frontend/entrypoints/inertia.js Adding inertia.js script tag to the application layout insert app/views/layouts/application.html.erb Adding Vite React Refresh tag to the application layout insert app/views/layouts/application.html.erb + gsub app/views/layouts/application.html.erb Copying example Inertia controller create app/controllers/inertia_example_controller.rb Adding a route for the example Inertia controller route get 'inertia-example', to: 'inertia_example#index' -Copying framework related files +Copying page assets create app/frontend/pages/InertiaExample.jsx + create app/frontend/pages/InertiaExample.module.css + create app/frontend/assets/react.svg + create app/frontend/assets/inertia.svg + create app/frontend/assets/vite_ruby.svg +Copying bin/dev + create bin/dev Inertia's Rails adapter successfully installed ``` With that done, you can now start the Rails server and the Vite development server (we recommend using [Overmind](https://github.com/DarthSim/overmind)): ```bash -overmind start -f Procfile.dev -# or -foreman start -f Procfile.dev +bin/dev ``` -And navigate to `http://127.0.0.1:5100/inertia-example` to see the example Inertia page. +And navigate to `http://localhost:3100/inertia-example` to see the example Inertia page. ### Scaffold generator diff --git a/docs/guide/server-side-setup.md b/docs/guide/server-side-setup.md index f0b9cd1..a43407a 100644 --- a/docs/guide/server-side-setup.md +++ b/docs/guide/server-side-setup.md @@ -15,83 +15,79 @@ bundle add inertia_rails ## Rails generator -If you plan to use Vite as your frontend build tool, you can use the `inertia_rails-contrib` gem to install and set up Inertia in a Rails application. **It requires the [Vite Rails](https://vite-ruby.netlify.app/guide/rails.html) gem to be installed and configured in the application.** +If you plan to use Vite as your frontend build tool, you can use the `inertia_rails-contrib` gem to install and set up Inertia in a Rails application. It automatically detects if the [Vite Rails](https://vite-ruby.netlify.app/guide/rails.html) gem is installed and will attempt to install it if not present. -To use the generator, execute the following command in the terminal: +To install and setup Inertia in a Rails application, execute the following command in the terminal: ```bash bundle add inertia_rails-contrib - bin/rails generate inertia:install ``` -This command will ask you for the frontend framework you are using (React, Vue, or Svelte) and will install the necessary dependencies and set up the application to work with Inertia. +This command will: +- Check for Vite Rails and install it if not present +- Ask you to choose your preferred frontend framework (React, Vue, or Svelte) +- Ask if you want to install Tailwind CSS +- Install necessary dependencies +- Set up the application to work with Inertia +- Copy example Inertia controller and views (can be skipped with the `--skip-example` option) Example output: ```bash $ bin/rails generate inertia:install - Installing Inertia's Rails adapter +Could not find a package.json file to install Inertia to. +Would you like to install Vite Ruby? (y/n) y + run bundle add vite_rails from "." +Vite Rails gem successfully installed + run bundle exec vite install from "." +Vite Rails successfully installed +Would you like to install Tailwind CSS? (y/n) y +Installing Tailwind CSS + run npm add tailwindcss postcss autoprefixer @tailwindcss/forms @tailwindcss/typography @tailwindcss/container-queries --silent from "." + create tailwind.config.js + create postcss.config.js + create app/frontend/entrypoints/application.css +Adding Tailwind CSS to the application layout + insert app/views/layouts/application.html.erb Adding Inertia's Rails adapter initializer create config/initializers/inertia_rails.rb Installing Inertia npm packages What framework do you want to use with Inertia? [react, vue, svelte] (react) - run npm add @inertiajs/inertia @inertiajs/react react react-dom from "." - -added 6 packages, removed 42 packages, and audited 69 packages in 8s - -18 packages are looking for funding - run `npm fund` for details - -2 moderate severity vulnerabilities - -Some issues need review, and may require choosing -a different dependency. - -Run `npm audit` for details. - run npm add --save-dev @vitejs/plugin-react from "." - -added 58 packages, and audited 127 packages in 6s - -22 packages are looking for funding - run `npm fund` for details - -2 moderate severity vulnerabilities - -Some issues need review, and may require choosing -a different dependency. - -Run `npm audit` for details. + run npm add @inertiajs/react react react-dom @vitejs/plugin-react --silent from "." Adding Vite plugin for react insert vite.config.ts prepend vite.config.ts -Add "type": "module", to the package.json file - gsub package.json -Copying inertia.js into Vite entrypoints +Copying inertia.js entrypoint create app/frontend/entrypoints/inertia.js Adding inertia.js script tag to the application layout insert app/views/layouts/application.html.erb Adding Vite React Refresh tag to the application layout insert app/views/layouts/application.html.erb + gsub app/views/layouts/application.html.erb Copying example Inertia controller create app/controllers/inertia_example_controller.rb Adding a route for the example Inertia controller route get 'inertia-example', to: 'inertia_example#index' -Copying framework related files +Copying page assets create app/frontend/pages/InertiaExample.jsx + create app/frontend/pages/InertiaExample.module.css + create app/frontend/assets/react.svg + create app/frontend/assets/inertia.svg + create app/frontend/assets/vite_ruby.svg +Copying bin/dev + create bin/dev Inertia's Rails adapter successfully installed ``` With that done, you can now start the Rails server and the Vite development server (we recommend using [Overmind](https://github.com/DarthSim/overmind)): ```bash -overmind start -f Procfile.dev -# or -foreman start -f Procfile.dev +bin/dev ``` -And navigate to `http://127.0.0.1:5100/inertia-example` to see the example Inertia page. +And navigate to `http://localhost:3100/inertia-example` to see the example Inertia page. That's it! You're all set up to start using Inertia in your Rails application. Check the guide on [creating pages](/guide/pages) to know more. 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..7e266f3 100644 --- a/lib/generators/inertia/install/install_generator.rb +++ b/lib/generators/inertia/install/install_generator.rb @@ -1,66 +1,53 @@ +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! - end + install_vite unless ruby_vite_installed? - 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 +55,136 @@ 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 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: true)) + 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: true)) + 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 + else + say_status "Could not find a Vite configuration files (`config/vite.json` & `vite.config.{ts,js,mjs,cjs}`).", nil + end + false + 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 +192,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