From 1712f3e9efa30e3e5f8270992eb2765791e59b7d Mon Sep 17 00:00:00 2001 From: Steve Polito Date: Fri, 13 Jan 2023 13:55:00 -0500 Subject: [PATCH] Support Rails 7 and Ruby 3 (#1106) This is a major upgrade that aims to make it so generated Rails applications with suspenders are running on Rails version 7 and Ruby version 3. We took the approach of making the least amount of changes possible. We were unable to support the latest version of Ruby because of a [failing build][]. A future commit could explore how to solve for this, but for now, we decided to just stick with version `3.0.5`. We settled on `3.0.5` and not `3.0.0` in an effort to suppress parser warnings. The biggest change is that we drop support for deprecated CSS tooling by removing [Bourbon][] and introduce [PostCSS][] and [cssbundling-rails][]. We also drop [Bitters][] as a runtime dependency. Add PostCSS Normalize --------------------- In order to use [normalize][], we need to use [PostCSS Normalize][] since we are using [cssbundling-rails][]. We need to move the invocation of the `suspenders:stylesheet_base` to the `leftovers` method because this method is invoked _after_ `finish_template` is called. We do this because we were running into issues where the call to `rails css:install:postcss` had not yet run, but our generator assumes that command has already run. The method must be named `leftovers` since that's what the [finish_template][] method expects. Additionally, we only call this generator if the caller has not passed anything to the `--css` option when generating a new app. This is because Rails handles other css frameworks like `bootstrap` and `tailwind` differently. Finally, we move the invocation of `outro` to the `leftovers` method since it ensures the message will be the last thing to be printed to the console. Add bin/yarn ------------ Now that we have moved off of [Webpacker][], the [bin/yarn][] binstub is no longer generated. Because of this, we need to manually have it added during a new app build. Introduce CalVer ---------------- Because this upgrade was a breaking change, we thought it was a good time to switch to [CalVer][]. Since this project is composed of other projects, every release is a potential breaking change. Using the date as the `major` version encodes this risk. Improve console output ---------------------- Ensures the invocation of all generator methods happens _before_ we print the outro. This is because a generator invokes all methods in the class in the order they are declared. Specifically, this meant invoking `generate_views` in the `suspenders_customization` method. Prior to this, `generate_views` was invoked after the `outro` was printed. [Webpacker]: https://github.com/rails/webpacker [failing build]: https://github.com/thoughtbot/suspenders/actions/runs/3418310707 [Bourbon]: https://github.com/thoughtbot/bourbon [PostCSS]: https://postcss.org [cssbundling-rails]: https://github.com/rails/cssbundling-rails [Bitters]: https://github.com/thoughtbot/bitters [normalize]: https://necolas.github.io/normalize.css/ [PostCSS Normalize]: https://github.com/csstools/postcss-normalize [finish_template]: https://github.com/rails/rails/blob/d5124b2522130785378238ba714029ce8ef9de97/railties/lib/rails/generators/rails/plugin/plugin_generator.rb#L283-L285 [bin/yarn]: https://github.com/rails/webpacker/blob/master/lib/install/bin/yarn [CalVer]: https://calver.org Co-authored-by: Aji Slater Co-authored-by: Eric Milford Co-authored-by: Stefanni Brasil --- .ruby-version | 2 +- NEWS.md | 13 ++++ README.md | 8 +-- docs/rails_7.md | 5 ++ lib/suspenders/app_builder.rb | 11 ++- lib/suspenders/generators/app_generator.rb | 18 ++--- .../generators/stylesheet_base_generator.rb | 25 ++----- lib/suspenders/generators/views_generator.rb | 5 -- lib/suspenders/version.rb | 4 +- spec/features/new_project_spec.rb | 67 +++++++++++++------ spec/suspenders/app_generator_spec.rb | 4 -- suspenders.gemspec | 3 +- templates/Gemfile.erb | 12 ++-- templates/_javascript.html.erb | 3 - templates/application.postcss.css | 1 + templates/application.scss | 7 -- templates/bin_yarn | 18 +++++ templates/descriptions/stylesheet_base.md | 5 +- templates/postcss.config.js | 8 +++ templates/suspenders_layout.html.erb.erb | 10 +-- 20 files changed, 138 insertions(+), 91 deletions(-) create mode 100644 docs/rails_7.md delete mode 100644 templates/_javascript.html.erb create mode 100644 templates/application.postcss.css delete mode 100644 templates/application.scss create mode 100755 templates/bin_yarn create mode 100644 templates/postcss.config.js diff --git a/.ruby-version b/.ruby-version index a4dd9dba4..eca690e73 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.4 +3.0.5 diff --git a/NEWS.md b/NEWS.md index c22a5c7f0..b1a99980c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,16 @@ +20230113.0 (January, 13, 2023) + +Support Rails 7 and Ruby 3. Introduce CalVer. + +* Upgraded: Ruby to 3.0.5 +* Upgraded: Supported Rails version to 7.0.0 +* Removed: Bourbon +* Removed: Bitters +* Removed: Autoprefixer Rails +* Added: cssbundling-rails +* Added: PostCSS Autoprefixer +* Added: PostCSS Normalize + 1.56.1 (July 17, 2022) Fixes a critical error with the previous release diff --git a/README.md b/README.md index 4961d4129..2e1723ae9 100644 --- a/README.md +++ b/README.md @@ -34,14 +34,10 @@ generated projectname/Gemfile. It includes application gems like: -* [Autoprefixer Rails](https://github.com/ai/autoprefixer-rails) for CSS vendor prefixes -* [Bourbon](https://github.com/thoughtbot/bourbon) for Sass mixins -* [Bitters](https://github.com/thoughtbot/bitters) for scaffold application styles * [Sidekiq](https://github.com/mperham/sidekiq) for background processing * [High Voltage](https://github.com/thoughtbot/high_voltage) for static pages * [Honeybadger](https://www.honeybadger.io/?affiliate=A43uwl) for exception notification -* [Normalize](https://necolas.github.io/normalize.css/) for resetting browser styles * [Oj](http://www.ohler.com/oj/) * [Postgres](https://github.com/ged/ruby-pg) for access to the Postgres database * [Rack Canonical Host](https://github.com/tylerhunt/rack-canonical-host) to @@ -105,6 +101,8 @@ Suspenders also comes with: * Configuration for [stylelint][stylelint] * The analytics adapter [Segment][segment] (and therefore config for Google Analytics, Intercom, Facebook Ads, Twitter Ads, etc.) +* [PostCSS Autoprefixer][autoprefixer] for CSS vendor prefixes +* [PostCSS Normalize][normalize] for resetting browser styles [setup]: https://robots.thoughtbot.com/bin-setup [compress]: https://robots.thoughtbot.com/content-compression-with-rack-deflater @@ -115,6 +113,8 @@ Suspenders also comes with: [hound]: https://houndci.com [stylelint]: https://stylelint.io/ [segment]: https://segment.com +[autoprefixer]: https://github.com/postcss/autoprefixer +[normalize]: https://github.com/csstools/postcss-normalize ## Heroku diff --git a/docs/rails_7.md b/docs/rails_7.md new file mode 100644 index 000000000..dee877ff2 --- /dev/null +++ b/docs/rails_7.md @@ -0,0 +1,5 @@ +# Rails 7 + +To create a new Rails 7 app with Suspenders, you need to have `node >= 14.6`. + +After the app is created, run the server with `bin/dev`. diff --git a/lib/suspenders/app_builder.rb b/lib/suspenders/app_builder.rb index d1f869ed6..8beb5547f 100644 --- a/lib/suspenders/app_builder.rb +++ b/lib/suspenders/app_builder.rb @@ -75,6 +75,11 @@ def provide_setup_script run "chmod a+x bin/setup" end + def provide_yarn_script + template "bin_yarn", "bin/yarn", force: true + run "chmod a+x bin/yarn" + end + def configure_generators config = <<-RUBY @@ -100,13 +105,13 @@ def configure_local_mail def setup_asset_host replace_in_file "config/environments/production.rb", - "# config.asset_host = 'http://assets.example.com'", + %(# config.asset_host = "http://assets.example.com"), 'config.asset_host = ENV.fetch("ASSET_HOST", ENV.fetch("APPLICATION_HOST"))' if File.exist?("config/initializers/assets.rb") replace_in_file "config/initializers/assets.rb", - "config.assets.version = '1.0'", - 'config.assets.version = (ENV["ASSETS_VERSION"] || "1.0")' + %(Rails.application.config.assets.version = "1.0"), + 'Rails.application.config.assets.version = (ENV["ASSETS_VERSION"] || "1.0")' end config = <<~EOD diff --git a/lib/suspenders/generators/app_generator.rb b/lib/suspenders/generators/app_generator.rb index f55d45aa4..94ea28c1d 100644 --- a/lib/suspenders/generators/app_generator.rb +++ b/lib/suspenders/generators/app_generator.rb @@ -31,17 +31,19 @@ class AppGenerator < Rails::Generators::AppGenerator class_option :skip_system_test, type: :boolean, default: true, desc: "Skip system test files" - class_option :skip_turbolinks, - type: :boolean, default: true, desc: "Skip turbolinks gem" - - class_option :skip_spring, type: :boolean, default: true, - desc: class_options[:skip_spring].description + class_option :css, + type: :string, default: "postcss", aliases: "-c", desc: "Choose CSS processor" def finish_template invoke :suspenders_customization super end + def leftovers + generate("suspenders:stylesheet_base") unless options[:api] || options[:css] != "postcss" + invoke :outro + end + def suspenders_customization invoke :customize_gemfile invoke :setup_development_environment @@ -57,7 +59,7 @@ def suspenders_customization invoke :remove_config_comment_lines invoke :remove_routes_comment_lines invoke :run_database_migrations - invoke :outro + invoke :generate_views end def customize_gemfile @@ -83,6 +85,7 @@ def setup_development_environment build :set_test_delivery_method build :raise_on_unpermitted_parameters build :provide_setup_script + build :provide_yarn_script build :configure_generators build :configure_i18n_for_missing_translations build :configure_quiet_assets @@ -150,7 +153,6 @@ def generate_default generate("suspenders:profiler") generate("suspenders:json") generate("suspenders:static") - generate("suspenders:stylesheet_base") unless options[:api] generate("suspenders:testing") generate("suspenders:ci") generate("suspenders:js_driver") @@ -180,7 +182,7 @@ def generate_views end def outro - say "Congratulations! You just pulled our suspenders." + say "\nCongratulations! You just pulled our suspenders." say honeybadger_outro end diff --git a/lib/suspenders/generators/stylesheet_base_generator.rb b/lib/suspenders/generators/stylesheet_base_generator.rb index 0b5ee52b8..15c12b058 100644 --- a/lib/suspenders/generators/stylesheet_base_generator.rb +++ b/lib/suspenders/generators/stylesheet_base_generator.rb @@ -2,29 +2,14 @@ module Suspenders class StylesheetBaseGenerator < Generators::Base - def add_stylesheet_gems - gem "bourbon", ">= 6.0.0" - Bundler.with_unbundled_env { run "bundle install" } - end - - def remove_prior_config - remove_file "app/assets/stylesheets/application.css" - end - - def add_css_config + def setup_normalize + run "yarn add postcss-normalize" copy_file( - "application.scss", - "app/assets/stylesheets/application.scss", + "application.postcss.css", + "app/assets/stylesheets/application.postcss.css", force: true ) - end - - def install_bitters - run "bitters install --path app/assets/stylesheets" - end - - def install_normalize_css - run "bin/yarn add normalize.css" + copy_file "postcss.config.js", "postcss.config.js", force: true end end end diff --git a/lib/suspenders/generators/views_generator.rb b/lib/suspenders/generators/views_generator.rb index b33a12b1b..68d94f65b 100644 --- a/lib/suspenders/generators/views_generator.rb +++ b/lib/suspenders/generators/views_generator.rb @@ -11,11 +11,6 @@ def create_shared_flashes copy_file "flashes_helper.rb", "app/helpers/flashes_helper.rb" end - def create_shared_javascripts - copy_file "_javascript.html.erb", - "app/views/application/_javascript.html.erb" - end - def create_shared_css_overrides copy_file "_css_overrides.html.erb", "app/views/application/_css_overrides.html.erb" diff --git a/lib/suspenders/version.rb b/lib/suspenders/version.rb index 08c3ac185..606b536ea 100644 --- a/lib/suspenders/version.rb +++ b/lib/suspenders/version.rb @@ -1,8 +1,8 @@ module Suspenders - RAILS_VERSION = "~> 6.1.6.1".freeze + RAILS_VERSION = "~> 7.0.0".freeze RUBY_VERSION = IO .read("#{File.dirname(__FILE__)}/../../.ruby-version") .strip .freeze - VERSION = "1.56.1".freeze + VERSION = "20230113.0".freeze end diff --git a/spec/features/new_project_spec.rb b/spec/features/new_project_spec.rb index f345079f1..be19738b0 100644 --- a/spec/features/new_project_spec.rb +++ b/spec/features/new_project_spec.rb @@ -13,9 +13,6 @@ expect(gemfile_file).to match( /^ruby "#{Suspenders::RUBY_VERSION}"$/o ) - expect(gemfile_file).to match( - /^gem "autoprefixer-rails"$/ - ) expect(gemfile_file).to match( /^gem "rails", "#{Suspenders::RAILS_VERSION}"$/o ) @@ -66,6 +63,14 @@ expect("bin/setup").to be_executable end + it "adds bin/yarn file" do + expect(File).to exist("#{project_path}/bin/yarn") + end + + it "makes bin/setup executable" do + expect("bin/yarn").to be_executable + end + it "adds support file for action mailer" do expect(File).to exist("#{project_path}/spec/support/action_mailer.rb") end @@ -255,10 +260,6 @@ expect(app_json_file).to match(/"name":\s*"#{app_name.dasherize}"/) end - def app_name - TestPaths::APP_NAME - end - it "adds high_voltage" do gemfile = IO.read("#{project_path}/Gemfile") expect(gemfile).to match(/high_voltage/) @@ -270,27 +271,53 @@ def app_name expect(gemfile).to match(/sassc-rails/) end - it "adds and configures bourbon" do + it "configures Timecop safe mode" do + spec_helper = read_project_file(%w[spec spec_helper.rb]) + expect(spec_helper).to match(/Timecop.safe_mode = true/) + end + + it "adds and configures a bundler strategy for css and js" do gemfile = read_project_file("Gemfile") - expect(gemfile).to match(/bourbon/) + expect(gemfile).to match(/cssbundling-rails/) + expect(gemfile).to match(/jsbundling-rails/) + expect(File).to exist("#{project_path}/postcss.config.js") + expect(File).to exist("#{project_path}/package.json") + expect(File).to exist("#{project_path}/bin/dev") + expect(File).to exist("#{project_path}/app/assets/stylesheets/application.postcss.css") + expect(File).to exist("#{project_path}/app/javascript/application.js") end - it "configures bourbon, and bitters" do - app_css = read_project_file(%w[app assets stylesheets application.scss]) - expect(app_css).to match( - /normalize\.css\/normalize.*bourbon.*base/m - ) + it "adds normalize.css" do + stylesheet = read_project_file %w[app assets stylesheets application.postcss.css] + dependencies = read_project_file %w[package.json] + configuration = read_project_file %w[postcss.config.js] + + expect(stylesheet).to include(%(@import "normalize.css")) + expect(dependencies).to include(%("postcss-normalize")) + expect(configuration).to include(%(require('postcss-normalize'))) end - it "doesn't use turbolinks" do - app_js = read_project_file(%w[app javascript packs application.js]) - expect(app_js).not_to match(/turbolinks/) + it "imports css and js" do + layout = read_project_file %w[app views layouts application.html.erb] + + expect(layout) + .to include(%(<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>)) + expect(layout) + .to include(%(<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>)) end - it "configures Timecop safe mode" do - spec_helper = read_project_file(%w[spec spec_helper.rb]) - expect(spec_helper).to match(/Timecop.safe_mode = true/) + it "loads security helpers" do + layout = read_project_file %w[app views layouts application.html.erb] + + expect(layout) + .to include(%(<%= csp_meta_tag %>)) + expect(layout) + .to include(%(<%= csrf_meta_tags %>)) + end + + def app_name + TestPaths::APP_NAME end def development_config diff --git a/spec/suspenders/app_generator_spec.rb b/spec/suspenders/app_generator_spec.rb index 01b545e4c..9e6c6b7db 100644 --- a/spec/suspenders/app_generator_spec.rb +++ b/spec/suspenders/app_generator_spec.rb @@ -4,8 +4,4 @@ it "includes ExitOnFailure" do expect(described_class.ancestors).to include(Suspenders::ExitOnFailure) end - - it "skips spring by default" do - expect(described_class.class_options[:skip_spring]&.default).to be true - end end diff --git a/suspenders.gemspec b/suspenders.gemspec index e235e354c..dde3b1cef 100644 --- a/suspenders.gemspec +++ b/suspenders.gemspec @@ -4,7 +4,7 @@ require "date" Gem::Specification.new do |s| s.required_ruby_version = ">= #{Suspenders::RUBY_VERSION}" - s.required_rubygems_version = ">= 2.7.4" + s.required_rubygems_version = ">= 3.0.0" s.authors = ["thoughtbot"] s.description = <<~HERE @@ -25,7 +25,6 @@ Gem::Specification.new do |s| s.summary = "Generate a Rails app using thoughtbot's best practices." s.version = Suspenders::VERSION - s.add_dependency "bitters", ">= 2.0.4" s.add_dependency "parser", ">= 3.0" s.add_dependency "bundler", ">= 2.1" s.add_dependency "rails", Suspenders::RAILS_VERSION diff --git a/templates/Gemfile.erb b/templates/Gemfile.erb index 872656e58..5442decb1 100644 --- a/templates/Gemfile.erb +++ b/templates/Gemfile.erb @@ -7,11 +7,14 @@ end ruby "<%= Suspenders::RUBY_VERSION %>" -<% unless options[:api] %> -gem "autoprefixer-rails" +<% if options[:api] %> +gem "sprockets", "< 4" <% end %> + gem "bootsnap", require: false +gem "cssbundling-rails" gem "honeybadger" +gem "jsbundling-rails" gem "pg" gem "puma" gem "rack-canonical-host" @@ -19,10 +22,11 @@ gem "rails", "<%= Suspenders::RAILS_VERSION %>" gem "recipient_interceptor" gem "sassc-rails" gem "skylight" -gem "sprockets", "< 4" +gem "sprockets-rails" +gem "stimulus-rails" gem "title" +gem "turbo-rails" gem "tzinfo-data", platforms: [:mingw, :x64_mingw, :mswin, :jruby] -gem "webpacker" group :development do gem "listen" diff --git a/templates/_javascript.html.erb b/templates/_javascript.html.erb deleted file mode 100644 index 639cce739..000000000 --- a/templates/_javascript.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -<%= javascript_pack_tag :application %> - -<%= yield :javascript %> diff --git a/templates/application.postcss.css b/templates/application.postcss.css new file mode 100644 index 000000000..9a2fbc1f5 --- /dev/null +++ b/templates/application.postcss.css @@ -0,0 +1 @@ +@import "normalize.css" diff --git a/templates/application.scss b/templates/application.scss deleted file mode 100644 index 95c01333c..000000000 --- a/templates/application.scss +++ /dev/null @@ -1,7 +0,0 @@ -@charset "utf-8"; - -@import "normalize.css/normalize"; - -@import "bourbon"; - -@import "base/base"; diff --git a/templates/bin_yarn b/templates/bin_yarn new file mode 100755 index 000000000..c42ebc61e --- /dev/null +++ b/templates/bin_yarn @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + yarn = ENV["PATH"].split(File::PATH_SEPARATOR) + .select { |dir| File.expand_path(dir) != __dir__ } + .product(["yarn", "yarnpkg", "yarn.cmd", "yarn.ps1"]) + .map { |dir, file| File.expand_path(file, dir) } + .find { |file| File.executable?(file) } + + if yarn + exec yarn, *ARGV + else + warn "Yarn executable was not detected in the system." + warn "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/templates/descriptions/stylesheet_base.md b/templates/descriptions/stylesheet_base.md index c92d9b8c5..cc03bee29 100644 --- a/templates/descriptions/stylesheet_base.md +++ b/templates/descriptions/stylesheet_base.md @@ -1,4 +1 @@ -Set up the basic tools needed for the thoughtbot design system. - -This integrates the Bourbon library of Scss mixins with Normalize.css as a -reset, then re-styles it by bringing Bitters into your project. +Adds PostCSS Normalize diff --git a/templates/postcss.config.js b/templates/postcss.config.js new file mode 100644 index 000000000..9c81e2163 --- /dev/null +++ b/templates/postcss.config.js @@ -0,0 +1,8 @@ +module.exports = { + plugins: [ + require('postcss-nesting'), + require('autoprefixer'), + require('postcss-normalize') + ], +} + diff --git a/templates/suspenders_layout.html.erb.erb b/templates/suspenders_layout.html.erb.erb index 77f4fb95b..36486938e 100644 --- a/templates/suspenders_layout.html.erb.erb +++ b/templates/suspenders_layout.html.erb.erb @@ -1,21 +1,23 @@ - - <%%# Configure default and controller-, and view-specific titles in config/locales/en.yml. For more see: https://github.com/calebthompson/title#usage %> <%%= title %> - <%%= stylesheet_link_tag :application, media: "all" %> + + <%%= csrf_meta_tags %> + <%%= csp_meta_tag %> + + <%%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> + <%%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %> <%%= render "flashes" -%> <%%= yield %> - <%%= render "javascript" %> <%%= render "css_overrides" %>