Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce suspenders:lint generator #1148

Merged
merged 1 commit into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/stylelint-config@3.0.0 @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
Comment on lines +23 to +25
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we need this for API only applications? Mailers requires styles, but those are usually inline.


def configure_eslint
copy_file "eslintrc.json", ".eslintrc.json"
end
Comment on lines +27 to +29
Copy link
Contributor Author

@stevepolitodesign stevepolitodesign Dec 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would an API only ever have JavaScript?


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
Comment on lines +57 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we allow this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be misunderstanding, but we are not allowing this since Enabled is set to false.

Lint/UselessAssignment:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we allow this?

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