From 1a03e9516736f1cfa2473037eaf8f4f35d86727b Mon Sep 17 00:00:00 2001 From: Steve Polito Date: Tue, 28 Nov 2023 16:58:06 -0500 Subject: [PATCH] Introduce `suspenders:styles` generator (#1145) Configures applications to use [PostCSS][1] or [Tailwind][2] via [cssbundling-rails][3]. Defaults to `PostCSS` with [modern-normalize][8], with the option to override via `--css=tailwind`. These options were pulled from the [supported list of options][4] in Rails. Also creates additional stylesheets if using PostCSS. We choose to use [cssbundling-rails][4] instead of [dartsass-rails][5] or [tailwindcss-rails][6] (or even just css) because we want to rely on Node to process the CSS. Although we could have chosen to avoid using Node altogether, we feel it's better to support it since we'll need it for additional generators, like [StyleLintGenerator][7], and to support [modern-normalize][8]. Updates `within_api_only_app` by allowing support to conditionally comment out the api configuration. This provided and opportunity to clean up existing setup steps. [1]: https://postcss.org [2]: https://tailwindcss.com [3]: https://github.com/rails/cssbundling-rails [4]: https://github.com/rails/rails/blob/438cad462638b02210fc48b700c29dcd0428a8b7/railties/lib/rails/generators/app_base.rb#L22 [5]: https://github.com/rails/dartsass-rails [6]: https://github.com/rails/tailwindcss-rails [7]: https://github.com/thoughtbot/suspenders/blob/main/lib/suspenders/generators/stylelint_generator.rb [8]: https://github.com/sindresorhus/modern-normalize [9]: https://tailwindcss.com/docs/functions-and-directives#layer --- NEWS.md | 1 + README.md | 16 ++ lib/generators/suspenders/styles_generator.rb | 60 +++++ .../accessibility_generator_test.rb | 6 +- .../suspenders/inline_svg_generator_test.rb | 6 +- .../suspenders/styles_generator_test.rb | 213 ++++++++++++++++++ test/test_helper.rb | 16 +- 7 files changed, 304 insertions(+), 14 deletions(-) create mode 100644 lib/generators/suspenders/styles_generator.rb create mode 100644 test/generators/suspenders/styles_generator_test.rb diff --git a/NEWS.md b/NEWS.md index 66e261da3..2da0cf764 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,7 @@ Unreleased * Introduce `suspenders:inline_svg` generator * Introduce `suspenders:factories` generator * Introduce `suspenders:advisories` generator +* Introduce `suspenders:styles` generator 20230113.0 (January, 13, 2023) diff --git a/README.md b/README.md index 8dbab30f7..dc0d21d78 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,22 @@ improvement for the viewer. [inline_svg]: https://github.com/jamesmartin/inline_svg +### Styles + +Configures applications to use [PostCSS][] or [Tailwind][] via +[cssbundling-rails][]. Defaults to PostCSS with [modern-normalize][], with the +option to override via `--css=tailwind`. + +Also creates a directory structure to store additional stylesheets if using +PostCSS. + +`bin/rails g suspenders:styles --css[postcss:tailwind]` + + [PostCSS]: https://postcss.org + [Tailwind]: https://tailwindcss.com + [cssbundling-rails]: https://github.com/rails/cssbundling-rails + [modern-normalize]: https://github.com/sindresorhus/modern-normalize + ## Contributing See the [CONTRIBUTING] document. diff --git a/lib/generators/suspenders/styles_generator.rb b/lib/generators/suspenders/styles_generator.rb new file mode 100644 index 000000000..512049810 --- /dev/null +++ b/lib/generators/suspenders/styles_generator.rb @@ -0,0 +1,60 @@ +module Suspenders + module Generators + class StylesGenerator < Rails::Generators::Base + include Suspenders::Generators::APIAppUnsupported + + CSS_OPTIONS = %w[tailwind postcss].freeze + + class_option :css, enum: CSS_OPTIONS, default: "postcss" + desc <<~TEXT + Configures applications to use PostCSS or Tailwind via cssbundling-rails. + Defaults to PostCSS with modern-normalize, with the option to override via + --css=tailwind. + + Also creates a directory structure to store additional stylesheets if using + PostCSS. + TEXT + + def add_cssbundling_rails_gem + gem "cssbundling-rails" + + Bundler.with_unbundled_env { run "bundle install" } + run "bin/rails css:install:#{css}" + end + + def build_directory_structure + return if is_tailwind? + + create_file "app/assets/stylesheets/base.css" + create_file "app/assets/stylesheets/components.css" + create_file "app/assets/stylesheets/utilities.css" + end + + # Modify if https://github.com/rails/cssbundling-rails/pull/139 is merged + def configure_application_stylesheet + return if is_tailwind? + + run "yarn add modern-normalize" + + append_to_file "app/assets/stylesheets/application.postcss.css" do + <<~TEXT + @import "modern-normalize"; + @import "base.css"; + @import "components.css"; + @import "utilities.css"; + TEXT + end + end + + private + + def css + @css ||= options["css"] + end + + def is_tailwind? + css == "tailwind" + end + end + end +end diff --git a/test/generators/suspenders/accessibility_generator_test.rb b/test/generators/suspenders/accessibility_generator_test.rb index 6728b6c75..c8ca95ef1 100644 --- a/test/generators/suspenders/accessibility_generator_test.rb +++ b/test/generators/suspenders/accessibility_generator_test.rb @@ -25,11 +25,7 @@ class AccessibilityGeneratorTest < Rails::Generators::TestCase end test "does not raise if API configuration is commented out" do - within_api_only_app do - path = app_root("config/application.rb") - content = File.binread(path).gsub!("config.api_only = true", "# config.api_only = true") - File.binwrite(path, content) - + within_api_only_app commented_out: true do run_generator assert_file app_root("Gemfile") do |file| diff --git a/test/generators/suspenders/inline_svg_generator_test.rb b/test/generators/suspenders/inline_svg_generator_test.rb index 141dfea1b..3ed0d3b15 100644 --- a/test/generators/suspenders/inline_svg_generator_test.rb +++ b/test/generators/suspenders/inline_svg_generator_test.rb @@ -24,11 +24,7 @@ class InlinveSvgGeneratorTest < Rails::Generators::TestCase end test "does not raise if API configuration is commented out" do - within_api_only_app do - path = app_root("config/application.rb") - content = File.binread(path).gsub!("config.api_only = true", "# config.api_only = true") - File.binwrite(path, content) - + within_api_only_app commented_out: true do run_generator assert_file app_root("Gemfile") do |file| diff --git a/test/generators/suspenders/styles_generator_test.rb b/test/generators/suspenders/styles_generator_test.rb new file mode 100644 index 000000000..a3980d264 --- /dev/null +++ b/test/generators/suspenders/styles_generator_test.rb @@ -0,0 +1,213 @@ +require "test_helper" +require "generators/suspenders/styles_generator" + +module Suspenders + module Generators + class StylesGenerator::DefaultTest < Rails::Generators::TestCase + include Suspenders::TestHelpers + + tests Suspenders::Generators::StylesGenerator + destination Rails.root + setup :prepare_destination + teardown :restore_destination + + test "raises if API only application" do + within_api_only_app do + assert_raises Suspenders::Generators::APIAppUnsupported::Error do + run_generator + end + end + end + + test "does not raise if API configuration is commented out" do + within_api_only_app(commented_out: true) do + run_generator + end + end + + test "adds gems to Gemfile" do + run_generator + + assert_file app_root("Gemfile") do |file| + assert_match "cssbundling-rails", file + end + end + + test "installs gems with Bundler" do + output = run_generator + + assert_match(/bundle install/, output) + end + + test "runs install script" do + output = run_generator + + assert_match(/bin\/rails css:install:postcss/, output) + end + + test "generator has a description" do + description = <<~TEXT + Configures applications to use PostCSS or Tailwind via cssbundling-rails. + Defaults to PostCSS with modern-normalize, with the option to override via + --css=tailwind. + + Also creates a directory structure to store additional stylesheets if using + PostCSS. + TEXT + + assert_equal description, generator_class.desc + end + + private + + def prepare_destination + touch "Gemfile" + touch "app/assets/stylesheets/application.postcss.css" + end + + def restore_destination + remove_file_if_exists "Gemfile" + remove_file_if_exists "package.json", root: true + remove_file_if_exists "yarn.lock", root: true + remove_file_if_exists "app/assets/stylesheets/application.postcss.css" + remove_file_if_exists "app/assets/stylesheets/base.css" + remove_file_if_exists "app/assets/stylesheets/components.css" + remove_file_if_exists "app/assets/stylesheets/utilities.css" + end + end + + class StylesGenerator::ClassOptionTest < Rails::Generators::TestCase + include Suspenders::TestHelpers + + tests Suspenders::Generators::StylesGenerator + destination Rails.root + setup :prepare_destination + teardown :restore_destination + + test "has a css option" do + option = generator_class.class_options[:css] + + assert_equal :string, option.type + assert_not option.required + assert_equal %w[tailwind postcss], option.enum + assert_equal "postcss", option.default + end + + test "raises if css option is unsupported" do + output = capture(:stderr) { run_generator %w[--css=unknown] } + + assert_match(/Expected '--css' to be one of/, output) + end + + private + + def prepare_destination + touch "Gemfile" + end + + def restore_destination + remove_file_if_exists "Gemfile" + remove_file_if_exists "package.json", root: true + remove_file_if_exists "yarn.lock", root: true + remove_file_if_exists "app/assets/stylesheets/application.postcss.css" + remove_file_if_exists "app/assets/stylesheets/base.css" + remove_file_if_exists "app/assets/stylesheets/components.css" + remove_file_if_exists "app/assets/stylesheets/utilities.css" + end + end + + class StylesGenerator::TailwindTest < Rails::Generators::TestCase + include Suspenders::TestHelpers + + tests Suspenders::Generators::StylesGenerator + destination Rails.root + setup :prepare_destination + teardown :restore_destination + + test "runs install script" do + output = run_generator %w[--css=tailwind] + + assert_match(/bin\/rails css:install:tailwind/, output) + end + + test "does not install normalize" do + output = run_generator %w[--css=tailwind] + + assert_no_match(/add.*modern-normalize/, output) + end + + test "does not create directory structure" do + run_generator %w[--css=tailwind] + + assert_no_file app_root("app/assets/stylesheets/base.css") + assert_no_file app_root("app/assets/stylesheets/components.css") + assert_no_file app_root("app/assets/stylesheets/utilities.css") + end + + private + + def prepare_destination + touch "Gemfile" + end + + def restore_destination + remove_file_if_exists "Gemfile" + remove_file_if_exists "package.json", root: true + remove_file_if_exists "yarn.lock", root: true + remove_file_if_exists "app/assets/stylesheets/base.css" + remove_file_if_exists "app/assets/stylesheets/components.css" + remove_file_if_exists "app/assets/stylesheets/utilities.css" + end + end + + class StylesGenerator::PostCssTest < Rails::Generators::TestCase + include Suspenders::TestHelpers + + tests Suspenders::Generators::StylesGenerator + destination Rails.root + setup :prepare_destination + teardown :restore_destination + + test "installs normalize and imports style sheets" do + output = run_generator %w[--css=postcss] + application_stylesheet = <<~TEXT + @import "modern-normalize"; + @import "base.css"; + @import "components.css"; + @import "utilities.css"; + TEXT + + assert_match(/add.*modern-normalize/, output) + + assert_file app_root("app/assets/stylesheets/application.postcss.css") do |file| + assert_equal application_stylesheet, file + end + end + + test "creates directory structure" do + run_generator + + assert_file app_root("app/assets/stylesheets/base.css") + assert_file app_root("app/assets/stylesheets/components.css") + assert_file app_root("app/assets/stylesheets/utilities.css") + end + + private + + def prepare_destination + touch "Gemfile" + touch "app/assets/stylesheets/application.postcss.css" + end + + def restore_destination + remove_file_if_exists "Gemfile" + remove_file_if_exists "package.json", root: true + remove_file_if_exists "yarn.lock", root: true + remove_file_if_exists "app/assets/stylesheets/application.postcss.css" + remove_file_if_exists "app/assets/stylesheets/base.css" + remove_file_if_exists "app/assets/stylesheets/components.css" + remove_file_if_exists "app/assets/stylesheets/utilities.css" + end + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index 3fb50d589..4fcaa7ebc 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -19,8 +19,9 @@ def app_root(path) Rails.root.join path end - def remove_file_if_exists(file) - path = app_root file + def remove_file_if_exists(file, **options) + root = options[:root] + path = root ? file : app_root(file) FileUtils.rm path if File.exist? path end @@ -43,7 +44,14 @@ def touch(file) FileUtils.touch path end - def within_api_only_app(&block) + def within_api_only_app(**options, &block) + commented_out = options[:commented_out] + set_config = if commented_out == true + "# config.api_only = true" + else + "config.api_only = true" + end + backup_file "config/application.rb" application_config = <<~RUBY require_relative "boot" @@ -57,7 +65,7 @@ class Application < Rails::Application config.autoload_lib(ignore: %w(assets tasks)) - config.api_only = true + #{set_config} end end RUBY