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