Skip to content

Commit

Permalink
Introduce suspenders:styles generator (#1145)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
stevepolitodesign committed May 10, 2024
1 parent f1960f1 commit d8cf034
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 14 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
60 changes: 60 additions & 0 deletions lib/generators/suspenders/styles_generator.rb
Original file line number Diff line number Diff line change
@@ -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
6 changes: 1 addition & 5 deletions test/generators/suspenders/accessibility_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
6 changes: 1 addition & 5 deletions test/generators/suspenders/inline_svg_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
213 changes: 213 additions & 0 deletions test/generators/suspenders/styles_generator_test.rb
Original file line number Diff line number Diff line change
@@ -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
16 changes: 12 additions & 4 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -57,7 +65,7 @@ class Application < Rails::Application
config.autoload_lib(ignore: %w(assets tasks))
config.api_only = true
#{set_config}
end
end
RUBY
Expand Down

0 comments on commit d8cf034

Please sign in to comment.