Skip to content

Commit

Permalink
Introduce 'suspenders:lint` generator (#1148)
Browse files Browse the repository at this point in the history
Closes #1107
Closes #1143

Creates a holistic linting solution that covers JavaScript, CSS, Ruby
and ERB.

Introduces [scripts][] that leverage [@thoughtbot/eslint-config][],
[@thoughtbot/stylelint-config][] and [prettier][].

Also introduces `.prettierrc` based off of our [Guides][].

We need to pin `stylelint` and `@thoughtbot/stlyelint-config` to
specific versions to account for this [open issue][]. Unfortunately,
running `yarn run lint:stylelint` results in deprecation warnings, which
will need to be addressed separately.

[scripts]: https://docs.npmjs.com/cli/v6/using-npm/scripts
[@thoughtbot/eslint-config]: https://github.com/thoughtbot/eslint-config
[@thoughtbot/stylelint-config]: https://github.com/thoughtbot/stylelint-config
[prettier]: https://prettier.io
[Guides]: https://github.com/thoughtbot/guides/blob/main/javascript/README.md#formatting
[open issue]: thoughtbot/stylelint-config#46

Introduces `rake standard` which also runs `erblint` to lint ERB files
via [better_html][], [erb_lint][] and [erblint-github][].

[better_html]: https://github.com/Shopify/better-html
[erb_lint]: https://github.com/Shopify/erb-lint
[erblint-github]: https://github.com/github/erblint-github

A future commit will ensure these linting rules are run during CI. In an
effort to support that future commit, we ensure to run `yarn run
fix:prettier` and `bundle exec standard:fix_unsafely` once the generator
is complete. Otherwise, CI would fail because of linting violations.

We call `standard:fix_unsafely` since `standard:fix` returns an error
status code on new Rails applications. Running `standard:fix_unsafely`
fixes this issue and returns a success status code.

It should be noted that we deliberately permit this generator to be
invoked on API only applications, because those applications can still
contain views, like ones used for mailers. However, a future commit could
explore removing the JavaScript linters.

Also improves the developer experience by introducing `with_test_suite`
helper, allowing the caller to run the generator in an application using
minitest or RSpec.
  • Loading branch information
stevepolitodesign committed May 10, 2024
1 parent 53f153f commit 547392b
Show file tree
Hide file tree
Showing 15 changed files with 630 additions and 1 deletion.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Unreleased
* Introduce `suspenders:advisories` generator
* Introduce `suspenders:styles` generator
* Introduce `suspenders:jobs` generator
* Introduce `suspenders:lint` generator

20230113.0 (January, 13, 2023)

Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,26 @@ improvement for the viewer.

[inline_svg]: https://github.com/jamesmartin/inline_svg

### Lint

Creates a holistic linting solution that covers JavaScript, CSS, Ruby and ERB.

Introduces NPM commands that leverage [@thoughtbot/eslint-config][],
[@thoughtbot/stylelint-config][] and [prettier][].

Also introduces `.prettierrc` based off of our [Guides][].

Introduces `rake standard` which also runs `erblint` to lint ERB files
via [better_html][], [erb_lint][] and [erblint-github][].

[@thoughtbot/eslint-config]: https://github.com/thoughtbot/eslint-config
[@thoughtbot/stylelint-config]: https://github.com/thoughtbot/stylelint-config
[prettier]: https://prettier.io
[Guides]: https://github.com/thoughtbot/guides/blob/main/javascript/README.md#formatting
[better_html]: https://github.com/Shopify/better-html
[erb_lint]: https://github.com/Shopify/erb-lint
[erblint-github]: https://github.com/github/erblint-github

### Styles

Configures applications to use [PostCSS][] or [Tailwind][] via
Expand Down
77 changes: 77 additions & 0 deletions lib/generators/suspenders/lint_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
module Suspenders
module Generators
class LintGenerator < Rails::Generators::Base
include Suspenders::Generators::Helpers

source_root File.expand_path("../../templates/lint", __FILE__)
desc "Creates a holistic linting solution that covers JavaScript, CSS, Ruby and ERB."

def install_dependencies
run "yarn add stylelint@^15.10.1 eslint @thoughtbot/[email protected] @thoughtbot/eslint-config npm-run-all prettier --dev"
end

def install_gems
gem_group :development, :test do
gem "better_html", require: false
gem "erb_lint", require: false
gem "erblint-github", require: false
gem "standard"
end
Bundler.with_unbundled_env { run "bundle install" }
end

def configure_stylelint
copy_file "stylelintrc.json", ".stylelintrc.json"
end

def configure_eslint
copy_file "eslintrc.json", ".eslintrc.json"
end

def configure_prettier
copy_file "prettierrc", ".prettierrc"
end

def configure_erb_lint
copy_file "erb-lint.yml", ".erb-lint.yml"
copy_file "config_better_html.yml", "config/better_html.yml"
copy_file "config_initializers_better_html.rb", "config/initializers/better_html.rb"
copy_file "erblint.rake", "lib/tasks/erblint.rake"
template "rubocop.yml.tt", ".rubocop.yml"

if default_test_suite?
copy_file "better_html_test.rb", "test/views/better_html_test.rb"
elsif rspec_test_suite?
copy_file "better_html_spec.rb", "spec/views/better_html_spec.rb"
end
end

def update_package_json
content = File.read package_json
json = JSON.parse content
json["scripts"] ||= {}

json["scripts"]["lint"] = "run-p lint:eslint lint:stylelint lint:prettier"
json["scripts"]["lint:eslint"] = "eslint --max-warnings=0 --no-error-on-unmatched-pattern 'app/javascript/**/*.js'"
json["scripts"]["lint:stylelint"] = "stylelint 'app/assets/stylesheets/**/*.css'"
json["scripts"]["lint:prettier"] = "prettier --check '**/*' --ignore-unknown"
json["scripts"]["fix:prettier"] = "prettier --write '**/*' --ignore-unknown"

File.write package_json, JSON.pretty_generate(json)
end

# This needs to be the last method definition to ensure everything is
# properly configured
def fix_violations
run "yarn run fix:prettier"
run "bundle exec rake standard:fix_unsafely"
end

private

def package_json
Rails.root.join("package.json")
end
end
end
end
17 changes: 17 additions & 0 deletions lib/generators/templates/lint/better_html_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require "spec_helper"

describe "ERB Implementation" do
def self.erb_lint
configuration = ActiveSupport::ConfigurationFile.parse(".erb-lint.yml")

ActiveSupport::OrderedOptions.new.merge!(configuration.deep_symbolize_keys!)
end

Rails.root.glob(erb_lint.glob).each do |template|
it "raises no html errors in #{template.relative_path_from(Rails.root)}" do
validator = BetterHtml::BetterErb::ErubiImplementation.new(template.read)

validator.validate!
end
end
end
17 changes: 17 additions & 0 deletions lib/generators/templates/lint/better_html_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
require "test_helper"

class ErbImplementationTest < ActiveSupport::TestCase
def self.erb_lint
configuration = ActiveSupport::ConfigurationFile.parse(".erb-lint.yml")

ActiveSupport::OrderedOptions.new.merge!(configuration.deep_symbolize_keys!)
end

Rails.root.glob(erb_lint.glob).each do |template|
test "html errors in #{template.relative_path_from(Rails.root)}" do
validator = BetterHtml::BetterErb::ErubiImplementation.new(template.read)

validator.validate!
end
end
end
2 changes: 2 additions & 0 deletions lib/generators/templates/lint/config_better_html.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
allow_single_quoted_attributes: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Rails.configuration.to_prepare do
if Rails.env.test?
require "better_html"

BetterHtml.config = BetterHtml::Config.new(Rails.configuration.x.better_html)

BetterHtml.config.template_exclusion_filter = proc { |filename| !filename.start_with?(Rails.root.to_s) }
end
end
63 changes: 63 additions & 0 deletions lib/generators/templates/lint/erb-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
glob: "app/views/**/*.{html,turbo_stream}{+*,}.erb"

linters:
AllowedScriptType:
enabled: true
allowed_types:
- "module"
- "text/javascript"
ErbSafety:
enabled: true
better_html_config: "config/better_html.yml"
GitHub::Accessibility::AvoidBothDisabledAndAriaDisabledCounter:
enabled: true
GitHub::Accessibility::AvoidGenericLinkTextCounter:
enabled: true
GitHub::Accessibility::DisabledAttributeCounter:
enabled: true
GitHub::Accessibility::IframeHasTitleCounter:
enabled: true
GitHub::Accessibility::ImageHasAltCounter:
enabled: true
GitHub::Accessibility::LandmarkHasLabelCounter:
enabled: true
GitHub::Accessibility::LinkHasHrefCounter:
enabled: true
GitHub::Accessibility::NestedInteractiveElementsCounter:
enabled: true
GitHub::Accessibility::NoAriaLabelMisuseCounter:
enabled: true
GitHub::Accessibility::NoPositiveTabIndexCounter:
enabled: true
GitHub::Accessibility::NoRedundantImageAltCounter:
enabled: true
GitHub::Accessibility::NoTitleAttributeCounter:
enabled: true
GitHub::Accessibility::SvgHasAccessibleTextCounter:
enabled: true
Rubocop:
enabled: true
rubocop_config:
inherit_from:
- .rubocop.yml

Lint/EmptyBlock:
Enabled: false
Layout/InitialIndentation:
Enabled: false
Layout/TrailingEmptyLines:
Enabled: false
Layout/TrailingWhitespace:
Enabled: false
Layout/LeadingEmptyLines:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: false
Style/MultilineTernaryOperator:
Enabled: false
Lint/UselessAssignment:
Exclude:
- "app/views/**/*"

EnableDefaultLinters: true
47 changes: 47 additions & 0 deletions lib/generators/templates/lint/erblint.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
module ERBLint
module RakeSupport
# Allow command line flags set in STANDARDOPTS (like MiniTest's TESTOPTS)
def self.argvify
if ENV["ERBLINTOPTS"]
ENV["ERBLINTOPTS"].split(/\s+/)
else
[]
end
end

# DELETE THIS FILE AFTER MERGE:
#
# * https://github.com/Shopify/better-html/pull/95
#
def self.backport!
BetterHtml::TestHelper::SafeErb::AllowedScriptType::VALID_JAVASCRIPT_TAG_TYPES.push("module")
end
end
end

desc "Lint templates with erb_lint"
task "erblint" do
require "erb_lint/cli"
require "erblint-github/linters"

ERBLint::RakeSupport.backport!

cli = ERBLint::CLI.new
success = cli.run(ERBLint::RakeSupport.argvify + ["--lint-all", "--format=compact"])
fail unless success
end

desc "Lint and automatically fix templates with erb_lint"
task "erblint:autocorrect" do
require "erb_lint/cli"
require "erblint-github/linters"

ERBLint::RakeSupport.backport!

cli = ERBLint::CLI.new
success = cli.run(ERBLint::RakeSupport.argvify + ["--lint-all", "--autocorrect"])
fail unless success
end

task "standard" => "erblint"
task "standard:fix" => "erblint:autocorrect"
7 changes: 7 additions & 0 deletions lib/generators/templates/lint/eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": ["@thoughtbot/eslint-config/prettier"],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
}
}
11 changes: 11 additions & 0 deletions lib/generators/templates/lint/prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"singleQuote": true,
"overrides": [
{
"files": ["**/*.css", "**/*.scss", "**/*.html"],
"options": {
"singleQuote": false
}
}
]
}
7 changes: 7 additions & 0 deletions lib/generators/templates/lint/rubocop.yml.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
AllCops:
TargetRubyVersion: <%= RUBY_VERSION %>

require: standard

inherit_gem:
standard: config/base.yml
3 changes: 3 additions & 0 deletions lib/generators/templates/lint/stylelintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "@thoughtbot/stylelint-config"
}
Loading

0 comments on commit 547392b

Please sign in to comment.