Skip to content

Commit

Permalink
Introduce 'suspenders:lint` generator
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 `eslint` and `stylelint` NPM commands that leverage
[@thoughtbot/eslint-config][] and [@thoughtbot/stylelint-config][].

```
yarn run eslint
yarn run stylelint
```

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

[@thoughtbot/eslint-config]: https://github.com/thoughtbot/eslint-config
[@thoughtbot/stylelint-config]: https://github.com/thoughtbot/stylelint-config
[Guides]: https://github.com/thoughtbot/guides/blob/main/javascript/README.md#formatting

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.

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.

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 Dec 11, 2023
1 parent 81e0e68 commit a90d52f
Show file tree
Hide file tree
Showing 13 changed files with 466 additions and 1 deletion.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,30 @@ 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 `eslint` and `stylelint` NPM commands that leverage
[@thoughtbot/eslint-config][] and [@thoughtbot/stylelint-config][].

```
yarn run eslint
yarn run stylelint
```

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
[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
67 changes: 67 additions & 0 deletions lib/generators/suspenders/lint_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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 eslint @thoughtbot/stylelint-config @thoughtbot/eslint-config --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"

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

# TODO: Consider extracting this into Rails
def update_package_json
content = File.read package_json
json = JSON.parse content
json["scripts"] ||= {}

json["scripts"]["eslint"] = "npx eslint 'app/javascript/**/*.js'"
json["scripts"]["stylelint"] = "npx stylelint 'app/assets/stylesheets/**/*.css'"

File.write package_json, JSON.pretty_generate(json)
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 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,5 @@
Rails.configuration.to_prepare do
config = ActiveSupport::ConfigurationFile.parse("config/better_html.yml")

BetterHtml.config = BetterHtml::Config.new(config)
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"
6 changes: 6 additions & 0 deletions lib/generators/templates/lint/eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": [
"@thoughtbot/eslint-config/base",
"@thoughtbot/eslint-config/prettier"
]
}
3 changes: 3 additions & 0 deletions lib/generators/templates/lint/prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"singleQuote": true
}
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 a90d52f

Please sign in to comment.