diff --git a/.circleci/config.yml b/.circleci/config.yml index e00d8173b..67d44246e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2.1 orbs: - browser-tools: circleci/browser-tools@1.5.2 + browser-tools: circleci/browser-tools@2.2.0 node: circleci/node@7.1.0 jobs: @@ -10,12 +10,10 @@ jobs: docker: - image: cimg/ruby:4.0.1-browsers environment: - BUNDLE_JOBS: 3 - BUNDLE_RETRY: 3 BUNDLE_PATH: vendor/bundle + NODE_VERSION: 22.17.1 PGHOST: 127.0.0.1 PGUSER: postgres - RACK_ENV: test RAILS_ENV: test - image: cimg/postgres:10.18 environment: @@ -25,22 +23,27 @@ jobs: steps: - checkout - - browser-tools/install-browser-tools: - install-chrome: false - install-chromedriver: false - node/install: - node-version: lts + node-version: 25.6.0 + + - node/install-packages: + pkg-manager: pnpm + + - browser-tools/install_firefox - run: - name: Which bundler? - command: bundle -v + name: Which versions? + command: | + bundle -v + node --version + pnpm --version # https://circleci.com/docs/2.0/caching/ - restore_cache: keys: - - bundle-v1-{{ checksum "Gemfile.lock" }} - - bundle-v1- + - bundle-v2-{{ checksum "Gemfile.lock" }} + - bundle-v2 - run: # Install Ruby dependencies name: Bundle Install @@ -50,41 +53,46 @@ jobs: bundle clean - save_cache: - key: bundle-v1-{{ checksum "Gemfile.lock" }} + key: bundle-v2-{{ checksum "Gemfile.lock" }} paths: - vendor/bundle - - node/install-packages: - pkg-manager: pnpm - - - run: - name: Build JS assets - command: pnpm build - - run: name: Wait for DB command: dockerize -wait tcp://localhost:5432 -timeout 1m - run: name: Database setup - command: bundle exec rake db:create db:schema:load --trace + command: bin/rails db:setup --trace - # - run: - # name: Brakeman - # command: bundle exec brakeman + - run: + name: Typescript + command: pnpm tscheck + + - run: + name: Find Unused ESLint Rules + command: pnpm eslint_find_unused_rules - run: name: ESLint command: pnpm eslint - run: - name: Stylelint - command: pnpm stylelint + name: Verify ESLint Autogen + command: bundle exec exe/eslint_autogen - run: name: Vitest command: pnpm vitest run --coverage + - run: + name: Brakeman + command: bundle exec brakeman + + - run: + name: Stylelint + command: pnpm stylelint + - run: name: Verify Stylelint Autogen command: bundle exec exe/stylelint_autogen @@ -94,37 +102,37 @@ jobs: command: bundle exec rubocop - run: - name: Run rspec in parallel + name: ✨ 🌈 ✨ Run Unit Tests ✨ 🌈 ✨ command: | - bundle exec rspec --exclude-pattern "spec/system/*_spec.rb" - # bundle exec rspec --profile 10 \ - # --format RspecJunitFormatter \ - # --out test_results/rspec.xml \ - # --format progress \ - # $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) - - - store_test_results: # https://circleci.com/docs/2.0/collect-test-data/ - path: test_results + TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \ + grep -v "spec/system" | \ + circleci tests split --split-by=timings)" + + echo "******************** TEST_FILES ************************" + echo "bundle exec rspec -s $TEST_FILES" + echo "********************************************************" + + COVERAGE=true bundle exec rspec \ + --format progress \ + --format RspecJunitFormatter \ + --out /tmp/test-results/rspec.xml \ + $TEST_FILES - run: - name: Run system tests + name: ✨ 🌈 ✨ Run System Tests ✨ 🌈 ✨ command: | - COVERAGE=false bundle exec rspec spec/system/ + TEST_FILES="$(circleci tests glob "spec/system/**/*_spec.rb" | \ + circleci tests split --split-by=timings)" -workflows: - build: - jobs: - - build - - # https://circleci.com/docs/2.0/workflows/#nightly-example - # https://circleci.com/docs/2.0/configuration-reference/#filters-1 - repeat: - jobs: - - build - triggers: - - schedule: - cron: "0,20,40 * * * *" - filters: - branches: - only: - - /.*ci-repeat.*/ + echo "******************** TEST_FILES ************************" + echo "bundle exec rspec -s $TEST_FILES" + echo "********************************************************" + + COVERAGE=false xvfb-run -a bundle exec rspec \ + --format progress \ + --format RspecJunitFormatter \ + --out /tmp/test-results/rspec.xml \ + $TEST_FILES + + - store_test_results: + path: test_results diff --git a/.eslint_todo.ts b/.eslint_todo.ts index fb8150f6a..2def3b075 100644 --- a/.eslint_todo.ts +++ b/.eslint_todo.ts @@ -1,162 +1,599 @@ // This configuration was generated by `exe/eslint_autogen` -// on 2025-12-16 22:19:02 UTC. +// on 2026-02-15 05:36:35 UTC. // The point is for the user to remove these configuration records // one by one as the offenses are removed from the code base. -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -const eslintTodo: Record[] = [ +import type {Linter} from "eslint"; + +const config: Linter.Config[] = [ + // Offense count: 7 { - files: ["app/javascript/application.ts"], + files: [ + "app/javascript/application.ts", + ], rules: { "@stylistic/comma-dangle": "off", + }, + }, + // Offense count: 5 + { + files: [ + "app/javascript/application.ts", + ], + rules: { "@stylistic/key-spacing": "off", + }, + }, + // Offense count: 2 + { + files: [ + "app/javascript/application.ts", + ], + rules: { "@stylistic/keyword-spacing": "off", + }, + }, + // Offense count: 15 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + "vitest.config.ts", + ], + rules: { "@stylistic/max-len": "off", + }, + }, + // Offense count: 1 + { + files: [ + "spec/javascript/setup.ts", + ], + rules: { + "@stylistic/multiline-comment-style": "off", + }, + }, + // Offense count: 8 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "@stylistic/multiline-ternary": "off", + }, + }, + // Offense count: 3 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/models/story_spec.ts", + ], + rules: { "@stylistic/no-extra-parens": "off", + }, + }, + // Offense count: 30 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "@stylistic/object-curly-spacing": "off", + }, + }, + // Offense count: 3 + { + files: [ + "app/javascript/application.ts", + "stylelint.config.mjs", + ], + rules: { "@stylistic/quote-props": "off", + }, + }, + // Offense count: 41 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + ], + rules: { "@stylistic/quotes": "off", + }, + }, + // Offense count: 1 + { + files: [ + "app/javascript/application.ts", + ], + rules: { "@stylistic/semi": "off", + }, + }, + // Offense count: 1 + { + files: [ + "app/javascript/application.ts", + ], + rules: { "@stylistic/space-before-blocks": "off", + }, + }, + // Offense count: 50 + { + files: [ + "app/javascript/application.ts", + ], + rules: { "@stylistic/space-before-function-paren": "off", + }, + }, + // Offense count: 4 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "@typescript-eslint/ban-ts-comment": "off", + }, + }, + // Offense count: 7 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "@typescript-eslint/explicit-function-return-type": "off", + }, + }, + // Offense count: 3 + { + files: [ + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "@typescript-eslint/init-declarations": "off", + }, + }, + // Offense count: 2 + { + files: [ + "app/javascript/application.ts", + ], + rules: { "@typescript-eslint/no-deprecated": "off", + }, + }, + // Offense count: 5 + { + files: [ + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "@typescript-eslint/no-empty-function": "off", + }, + }, + // Offense count: 9 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "@typescript-eslint/no-unsafe-argument": "off", + }, + }, + // Offense count: 45 + { + files: [ + "app/javascript/application.ts", + "eslint.config.mts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "@typescript-eslint/no-unsafe-assignment": "off", + }, + }, + // Offense count: 181 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "@typescript-eslint/no-unsafe-call": "off", + }, + }, + // Offense count: 266 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "@typescript-eslint/no-unsafe-member-access": "off", + }, + }, + // Offense count: 6 + { + files: [ + "app/javascript/application.ts", + ], + rules: { "@typescript-eslint/no-unsafe-return": "off", + }, + }, + // Offense count: 1 + { + files: [ + "app/javascript/application.ts", + ], + rules: { "@typescript-eslint/prefer-destructuring": "off", + }, + }, + // Offense count: 1 + { + files: [ + "app/javascript/application.ts", + ], + rules: { "@typescript-eslint/prefer-nullish-coalescing": "off", + }, + }, + // Offense count: 1 + { + files: [ + "app/javascript/application.ts", + ], + rules: { "@typescript-eslint/prefer-optional-chain": "off", + }, + }, + // Offense count: 24 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/models/story_spec.ts", + ], + rules: { "@typescript-eslint/strict-boolean-expressions": "off", + }, + }, + // Offense count: 9 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "camelcase": "off", + }, + }, + // Offense count: 1 + { + files: [ + "spec/javascript/setup.ts", + ], + rules: { + "capitalized-comments": "off", + }, + }, + // Offense count: 13 + { + files: [ + "app/javascript/application.ts", + ], + rules: { "curly": "off", + }, + }, + // Offense count: 2 + { + files: [ + "app/javascript/application.ts", + ], + rules: { "eqeqeq": "off", + }, + }, + // Offense count: 111 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "func-names": "off", + }, + }, + // Offense count: 3 + { + files: [ + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "func-style": "off", + }, + }, + // Offense count: 15 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "id-length": "off", + }, + }, + // Offense count: 1 + { + files: [ + "app/javascript/application.ts", + ], + rules: { "import/no-unresolved": "off", + }, + }, + // Offense count: 12 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/views/story_view_spec.ts", + "vitest.config.ts", + ], + rules: { "max-len": "off", + }, + }, + // Offense count: 1 + { + files: [ + "app/javascript/application.ts", + ], + rules: { "max-lines": "off", - "new-cap": "off", - "no-plusplus": "off", - "no-ternary": "off", - "object-shorthand": "off", - "prefer-arrow-callback": "off", - "prefer-named-capture-group": "off", - "prefer-template": "off", - "require-unicode-regexp": "off", - "sort-keys": "off", - "sort-keys-fix/sort-keys-fix": "off", - "vars-on-top": "off", }, }, + // Offense count: 3 { - files: ["eslint.config.mts"], + files: [ + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], rules: { - "@typescript-eslint/no-unsafe-assignment": "off", + "max-lines-per-function": "off", }, }, + // Offense count: 1 { - files: ["spec/javascript/setup.ts"], + files: [ + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { + "max-statements": "off", + }, + }, + // Offense count: 4 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], rules: { - "@stylistic/max-len": "off", - "@stylistic/multiline-comment-style": "off", - "@stylistic/object-curly-spacing": "off", - "@stylistic/quotes": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-call": "off", - "capitalized-comments": "off", - "func-names": "off", - "id-length": "off", - "max-len": "off", "new-cap": "off", - "prefer-named-capture-group": "off", - "require-unicode-regexp": "off", - "sort-imports": "off", - "sort-keys": "off", - "sort-keys-fix/sort-keys-fix": "off", - "vitest/require-hook": "off", }, }, + // Offense count: 2 { - files: ["spec/javascript/spec/models/story_spec.ts"], + files: [ + "spec/javascript/spec/models/story_spec.ts", + ], rules: { - "@stylistic/max-len": "off", - "@stylistic/no-extra-parens": "off", - "@stylistic/object-curly-spacing": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "@typescript-eslint/strict-boolean-expressions": "off", - "camelcase": "off", - "func-names": "off", - "max-lines-per-function": "off", "no-implicit-coercion": "off", + }, + }, + // Offense count: 2 + { + files: [ + "spec/javascript/spec/models/story_spec.ts", + ], + rules: { "no-multi-assign": "off", - "prefer-arrow-callback": "off", - "sort-imports": "off", - "sort-keys": "off", - "sort-keys-fix/sort-keys-fix": "off", - "vitest/prefer-lowercase-title": "off", }, }, + // Offense count: 1 { - files: ["spec/javascript/spec/views/story_view_spec.ts"], + files: [ + "spec/javascript/spec/views/story_view_spec.ts", + ], rules: { - "@stylistic/max-len": "off", - "@stylistic/multiline-ternary": "off", - "@stylistic/object-curly-spacing": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/init-declarations": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-unsafe-argument": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "@typescript-eslint/no-unsafe-call": "off", - "@typescript-eslint/no-unsafe-member-access": "off", - "camelcase": "off", - "func-names": "off", - "func-style": "off", - "id-length": "off", - "max-len": "off", - "max-lines-per-function": "off", - "max-statements": "off", - "new-cap": "off", "no-negated-condition": "off", + }, + }, + // Offense count: 1 + { + files: [ + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "no-param-reassign": "off", + }, + }, + // Offense count: 2 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "no-plusplus": "off", + }, + }, + // Offense count: 4 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "no-ternary": "off", + }, + }, + // Offense count: 47 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "object-shorthand": "off", + }, + }, + // Offense count: 1 + { + files: [ + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "one-var": "off", + }, + }, + // Offense count: 59 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "prefer-arrow-callback": "off", + }, + }, + // Offense count: 4 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + ], + rules: { + "prefer-named-capture-group": "off", + }, + }, + // Offense count: 1 + { + files: [ + "app/javascript/application.ts", + ], + rules: { + "prefer-template": "off", + }, + }, + // Offense count: 4 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + ], + rules: { + "require-unicode-regexp": "off", + }, + }, + // Offense count: 3 + { + files: [ + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "sort-imports": "off", + }, + }, + // Offense count: 26 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "sort-keys": "off", + }, + }, + // Offense count: 26 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/setup.ts", + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "sort-keys-fix/sort-keys-fix": "off", + }, + }, + // Offense count: 8 + { + files: [ + "app/javascript/application.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "vars-on-top": "off", + }, + }, + // Offense count: 3 + { + files: [ + "spec/javascript/spec/views/story_view_spec.ts", + ], + rules: { "vitest/no-hooks": "off", - "vitest/prefer-lowercase-title": "off", - "vitest/require-hook": "off", }, }, + // Offense count: 4 { - files: ["stylelint.config.mjs"], + files: [ + "spec/javascript/spec/models/story_spec.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], rules: { - "@stylistic/quote-props": "off", + "vitest/prefer-lowercase-title": "off", }, }, + // Offense count: 4 { - files: ["vitest.config.ts"], + files: [ + "spec/javascript/setup.ts", + "spec/javascript/spec/views/story_view_spec.ts", + ], rules: { - "@stylistic/function-paren-newline": "off", - "@stylistic/max-len": "off", - "max-len": "off", + "vitest/require-hook": "off", }, }, ]; -export default eslintTodo; +export default config; diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 725f51393..6231ee96c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 400` -# on 2026-02-14 02:53:21 UTC using RuboCop version 1.84.1. +# on 2026-02-15 19:40:41 UTC using RuboCop version 1.84.1. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -187,10 +187,9 @@ RSpec/VerifiedDoubles: - 'spec/commands/feed/find_new_stories_spec.rb' - 'spec/tasks/remove_old_stories_spec.rb' -# Offense count: 2 +# Offense count: 1 Rails/Env: Exclude: - - 'config/routes.rb' - 'spec/rails_helper.rb' # Offense count: 2 @@ -252,7 +251,7 @@ Rails/Validation: Exclude: - 'app/models/story.rb' -# Offense count: 21 +# Offense count: 23 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: brackets, fetch @@ -266,8 +265,8 @@ Style/HashLookupMethod: - 'app/controllers/feeds_controller.rb' - 'app/controllers/stories_controller.rb' - 'config/application.rb' + - 'exe/eslint_autogen' - 'exe/stylelint_autogen' - - 'spec/javascript/test_controller.rb' - 'spec/repositories/feed_repository_spec.rb' - 'spec/repositories/story_repository_spec.rb' - 'spec/repositories/user_repository_spec.rb' diff --git a/Gemfile b/Gemfile index 58cf31d64..5f42e9cf7 100644 --- a/Gemfile +++ b/Gemfile @@ -27,6 +27,7 @@ gem "thread" gem "will_paginate" group :development do + gem "brakeman", require: false gem "rubocop", require: false gem "rubocop-capybara", require: false gem "rubocop-factory_bot", require: false @@ -51,6 +52,7 @@ end group :test do gem "axe-core-rspec" + gem "rspec_junit_formatter" gem "selenium-webdriver" gem "webdrivers" gem "with_model" diff --git a/Gemfile.lock b/Gemfile.lock index 41f1f7008..0c3ce7693 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,6 +97,8 @@ GEM bindex (0.8.1) bootsnap (1.22.0) msgpack (~> 1.2) + brakeman (8.0.2) + racc builder (3.3.0) byebug (13.0.0) reline (>= 0.6.0) @@ -321,6 +323,8 @@ GEM rspec-mocks (~> 3.13) rspec-support (~> 3.13) rspec-support (3.13.7) + rspec_junit_formatter (0.6.0) + rspec-core (>= 2, < 4, != 2.12.0) rubocop (1.84.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) @@ -434,6 +438,7 @@ DEPENDENCIES axe-core-rspec bcrypt bootsnap + brakeman capybara coveralls_reborn debug @@ -453,6 +458,7 @@ DEPENDENCIES rails (~> 8.1.0) rspec rspec-rails + rspec_junit_formatter rubocop rubocop-capybara rubocop-factory_bot diff --git a/config/brakeman.ignore b/config/brakeman.ignore new file mode 100644 index 000000000..8bbe462b8 --- /dev/null +++ b/config/brakeman.ignore @@ -0,0 +1,10 @@ +{ + "ignored_warnings": [ + { + "fingerprint": "6f5239fb87c64764d0c209014deb5cf504c2c10ee424bd33590f0a4f22e01d8f", + "note": "False positive: load_defaults(8.0) enables default_protect_from_forgery, but brakeman doesn't parse config inside `Stringer::Application` (:: syntax). See https://github.com/presidentbeef/brakeman/issues/XXXX" + } + ], + "updated": "2026-02-15", + "brakeman_version": "8.0.2" +} diff --git a/docker/init_or_update_env.rb b/docker/init_or_update_env.rb index 6b2845c6b..0b72f79c0 100644 --- a/docker/init_or_update_env.rb +++ b/docker/init_or_update_env.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true +require "securerandom" + module Secrets def self.generate_secret(length) - `openssl rand -hex #{length}`.strip + SecureRandom.hex(length) end end diff --git a/exe/eslint_autogen b/exe/eslint_autogen new file mode 100755 index 000000000..eecee6eb7 --- /dev/null +++ b/exe/eslint_autogen @@ -0,0 +1,58 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "active_support/all" +require "stringio" + +TODO_FILE_PATH = "./.eslint_todo.ts" +HEADING = <<~COMMENTS.freeze + // This configuration was generated by `exe/eslint_autogen` + // on #{Time.now.utc}. + // The point is for the user to remove these configuration records + // one by one as the offenses are removed from the code base. +COMMENTS + +File.write(TODO_FILE_PATH, "export default []") +json = `pnpm --silent eslint --format json` +results = JSON.parse(json) + +by_rule = + results.each_with_object({}) do |result, hash| + result.fetch("messages").each do |message| + rule_id = message.fetch("ruleId") + next if rule_id.nil? + + hash[rule_id] ||= [] + hash[rule_id] << result.fetch("filePath") + end + end + +output = StringIO.new +output.puts(HEADING) +output.puts +output.puts("import type {Linter} from \"eslint\";") +output.puts +output.puts("const config: Linter.Config[] = [") + +by_rule.sort.each do |rule, file_paths| + output.puts(" // Offense count: #{file_paths.length}") + output.puts(" {") + output.puts(" files: [") + + file_paths.uniq.sort.each do |file_path| + relative_path = file_path.sub("#{Dir.pwd}/", "") + output.puts(" \"#{relative_path}\",") + end + + output.puts(" ],") + output.puts(" rules: {") + output.puts(" \"#{rule}\": \"off\",") + output.puts(" },") + output.puts(" },") +end + +output.puts("];") +output.puts +output.puts("export default config;") + +File.write(TODO_FILE_PATH, output.string) diff --git a/package.json b/package.json index 705b8be6d..ec32d64cb 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "scripts": { "build": "esbuild app/javascript/*.* --bundle --sourcemap --format=iife --outdir=app/assets/builds --public-path=/assets --alias:jquery=./node_modules/jquery/jquery.js --alias:bootstrap=./node_modules/bootstrap/dist/js/bootstrap.js --alias:jquery-visible=./node_modules/jquery-visible/jquery.visible.min.js", "eslint": "eslint ./ --cache --max-warnings=0", + "eslint_find_unused_rules": "eslint-find-rules --unused --flatConfig --no-core eslint.config.mts", "stylelint": "stylelint 'app/assets/stylesheets/**/*.css'", "tscheck": "tsc --noEmit", "pretest": "pnpm tscheck && pnpm eslint", diff --git a/spec/support/assets.rb b/spec/support/assets.rb new file mode 100644 index 000000000..fc00ec5e5 --- /dev/null +++ b/spec/support/assets.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.before(:all) do + system("pnpm build > /dev/null 2>&1", exception: true) + end +end