Skip to content

Upgrade to ESLint v10 and modernize lint config#284

Open
spatten wants to merge 2 commits intomainfrom
upgrade-eslint-v10
Open

Upgrade to ESLint v10 and modernize lint config#284
spatten wants to merge 2 commits intomainfrom
upgrade-eslint-v10

Conversation

@spatten
Copy link
Copy Markdown
Contributor

@spatten spatten commented Apr 1, 2026

Overview

Upgrade eslint from v9 to v10 and rewrite eslint.config.mjs to use native flat config, removing FlatCompat and several dead/incompatible dependencies.

This replaces the piecemeal @eslint/js v10 bump from #280, which created an unsupported split (@eslint/js v10 with eslint v9 runtime). This PR upgrades everything together.

Key changes:

  • eslint 9 → 10, @eslint/js 9 → 10
  • Replaced @typescript-eslint/eslint-plugin + @typescript-eslint/parser with the unified typescript-eslint package
  • Replaced eslint-plugin-import with eslint-plugin-import-x (actively maintained fork with eslint v10 support)
  • Removed 10 dead/incompatible packages: eslint-config-airbnb-base, eslint-config-standard, eslint-plugin-standard, eslint-plugin-node, eslint-plugin-react, eslint-plugin-promise, eslint-plugin-n, @eslint/compat, @eslint/eslintrc
  • Rewrote eslint.config.mjs to use tseslint.config() with native flat config — no more FlatCompat or fixupConfigRules/fixupPluginRules wrappers
  • devDependencies reduced from 16 to 8

All existing lint rules are preserved. The import/extensions rule was disabled (alongside the already-disabled import/no-unresolved) since TypeScript handles module resolution, and eslint-plugin-import-x treats .js extensions in TS imports differently than the old plugin.

Why remove airbnb-base and standard?

These presets are dead projects that don't support eslint v10:

  • eslint-config-airbnb-base — last published Nov 2021, peer dep eslint ^7.32.0 || ^8.2.0
  • eslint-config-standard — last published Jun 2023, peer dep eslint ^8.0.1

There's no v10-compatible version of either, and no maintained fork. Most of what they provided falls into three buckets:

  1. Rules already explicitly overridden in the config (~50 rules with specific settings) — these are preserved.
  2. Rules covered by eslint:recommended + tseslint.configs.recommended — correctness rules like no-undef, no-unused-vars, etc.
  3. Style/opinion rules unique to airbnb or standard (e.g. requiring ===, specific spacing preferences) — these are the ones we lost. If any are missed, they can be added back explicitly.

Risks

  • Low risk overall — this only affects dev tooling (linting). No runtime code changed, no dist/ output changed. The action itself is identical.
  • import/extensions disabled — previously enforced "never use .js/.ts extensions in imports." Now disabled because eslint-plugin-import-x handles TypeScript ESM .js extensions differently. No functional impact since TypeScript validates this at compile time.
  • Three new eslint:recommended rules from v10 (no-unassigned-vars, no-useless-assignment, preserve-caught-error) are now active. They passed clean on the current code, but could flag issues in future code — which is a positive.

Checklist

  • If I changed code, I ran yarn build and committed resulting changes.
  • I added an example exercising this PRs functionality to .github/workflows/test.yml or explained why it doesn't make sense to do so.
    • N/A: This is a dev tooling change (ESLint config). The existing lint CI job validates the config works correctly. No workflow test changes needed.

Important

After merging, make sure to create a new GitHub release and associated tag for this release.
You can either create the tag locally and then create a corresponding GitHub release,
or just create both the tag and release using the GitHub Release UI.

Additionally, if this is not a breaking change, make sure to update the v1 tag:

# Check out the tag you want to set as `v1`.
git checkout $TAG

# Delete and re-create the `v1` tag.
git tag -d v1 && git push origin :refs/tags/v1 && git tag v1 && git push origin tag v1

🤖 Generated with Claude Code

Upgrade eslint from v9 to v10 and rewrite eslint.config.mjs to use native
flat config, removing FlatCompat and several dead/incompatible dependencies.

- Bump eslint 9→10, @eslint/js 9→10
- Replace @typescript-eslint/eslint-plugin + parser with unified typescript-eslint
- Replace eslint-plugin-import with eslint-plugin-import-x (maintained fork)
- Remove dead packages: eslint-config-airbnb-base, eslint-config-standard,
  eslint-plugin-standard, eslint-plugin-node, eslint-plugin-react,
  eslint-plugin-promise, eslint-plugin-n, @eslint/compat, @eslint/eslintrc
- devDependencies reduced from 16 to 8

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@spatten spatten requested a review from a team as a code owner April 1, 2026 21:58
@spatten spatten requested a review from Conor-FOSSA April 1, 2026 21:58
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

Walkthrough

This change refactors the ESLint configuration to use the typescript-eslint configuration wrapper (tseslint.config) instead of the previous FlatCompat/defineConfig approach. The plugin setup transitions from eslint-plugin-standard and eslint-plugin-import to eslint-plugin-import-x. Language options are simplified by removing explicit TypeScript parser declarations. Import extension enforcement is disabled, and specific TypeScript rules are updated. The package.json is updated to reflect ESLint v10, adding typescript-eslint as a primary dependency while removing several legacy ESLint plugins and configurations.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main changes: upgrading ESLint from v9 to v10 and modernizing the configuration to use native flat config.
Description check ✅ Passed The PR description is comprehensive and complete, addressing all template sections with detailed context about the upgrade, key changes, rationale for removals, and risk assessment.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@eslint.config.mjs`:
- Line 165: The base ESLint rule "indent" (currently set as indent: ["error",
2]) can mis-handle TypeScript syntax; replace or disable it by setting indent:
"off" and rely on your formatter (e.g., Prettier) to enforce indentation, or
alternatively swap to the TypeScript-aware rule (historically
`@typescript-eslint/indent`) if you choose to keep ESLint enforcing indent; update
the rule definition where indent: ["error", 2] is declared to either indent:
"off" or the TypeScript-specific rule.
- Around line 16-21: The globals configuration currently expands browser globals
by mapping each key to "off" using
Object.fromEntries(Object.entries(globals.browser).map(...)); simplify by either
removing that expansion entirely if you don't need to override browser globals,
or replace it with a static empty object to explicitly disable them when
overriding upstream (i.e., stop using Object.fromEntries/Object.entries mapping
and instead omit the browser spread or use an explicit {}). Update the globals
object where "globals" is declared to apply one of these simpler approaches.
- Around line 169-180: The base ESLint rule "no-use-before-define" conflicts
with the TypeScript-aware rule "@typescript-eslint/no-use-before-define"
(different `variables` settings), so disable the base rule to avoid false
positives: update the ESLint config to turn off "no-use-before-define" (or
remove it) and keep only "@typescript-eslint/no-use-before-define" with the
existing options; target the rule key "no-use-before-define" in the config for
the change.

In `@package.json`:
- Around line 20-27: The package.json references non-existent versions for
ESLint-related packages; update the dependency versions for "eslint",
"@eslint/js", "typescript-eslint" (package name likely "@typescript-eslint/*" or
"typescript-eslint" based on your lockfile) and "eslint-plugin-import-x" to the
corresponding published versions on npm (e.g., set "eslint" and "@eslint/js" to
a valid 9.x release, set "eslint-plugin-import-x" to 4.9.4, and use the correct
`@typescript-eslint` package names/versions such as the latest stable
`@typescript-eslint/parser` and `@typescript-eslint/eslint-plugin` matching
TypeScript 5.x), then run npm install to verify; ensure you update the exact
package keys in package.json (symbols: "eslint", "@eslint/js",
"eslint-plugin-import-x", and the typescript-eslint package entries) rather than
adding new keys.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2c0f6861-f067-45e9-9571-389ccad8b375

📥 Commits

Reviewing files that changed from the base of the PR and between fca84c8 and 834bf85.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (2)
  • eslint.config.mjs
  • package.json

Comment thread eslint.config.mjs
Comment on lines +16 to +21
globals: {
...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, "off"])),
...globals.node,
...globals.mocha,
GLOBAL: true,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider simplifying globals configuration.

The pattern to disable browser globals by mapping each to "off" works, but is verbose. If the goal is simply to not include browser globals, you could omit them entirely:

       globals: {
-        ...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, "off"])),
         ...globals.node,
         ...globals.mocha,
         GLOBAL: true,
       },

If you need to explicitly disable browser globals to override upstream configs, the current approach is valid.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
globals: {
...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, "off"])),
...globals.node,
...globals.mocha,
GLOBAL: true,
},
globals: {
...globals.node,
...globals.mocha,
GLOBAL: true,
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eslint.config.mjs` around lines 16 - 21, The globals configuration currently
expands browser globals by mapping each key to "off" using
Object.fromEntries(Object.entries(globals.browser).map(...)); simplify by either
removing that expansion entirely if you don't need to override browser globals,
or replace it with a static empty object to explicitly disable them when
overriding upstream (i.e., stop using Object.fromEntries/Object.entries mapping
and instead omit the browser spread or use an explicit {}). Update the globals
object where "globals" is declared to apply one of these simpler approaches.

Comment thread eslint.config.mjs

"import/no-named-as-default": 0,
"import/no-deprecated": 2,
indent: ["error", 2],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Base indent rule may misbehave with TypeScript syntax.

The base ESLint indent rule can struggle with TypeScript-specific syntax (generics, type assertions, decorators). The @typescript-eslint/indent rule existed to address this but was deprecated in favor of dedicated formatters like Prettier.

If you encounter indentation errors on valid TS code, consider either:

  • Adding indent: "off" and relying on a formatter
  • Using @typescript-eslint/indent (deprecated but functional)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eslint.config.mjs` at line 165, The base ESLint rule "indent" (currently set
as indent: ["error", 2]) can mis-handle TypeScript syntax; replace or disable it
by setting indent: "off" and rely on your formatter (e.g., Prettier) to enforce
indentation, or alternatively swap to the TypeScript-aware rule (historically
`@typescript-eslint/indent`) if you choose to keep ESLint enforcing indent; update
the rule definition where indent: ["error", 2] is declared to either indent:
"off" or the TypeScript-specific rule.

Comment thread eslint.config.mjs Outdated
Comment on lines +169 to +180
"@typescript-eslint/no-use-before-define": ["error", {
variables: false,
typedefs: false,
functions: false,
classes: false,
}],

"no-use-before-define": ["error", {
variables: true,
functions: false,
classes: false,
}],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

typescript-eslint no-use-before-define rule documentation - should base ESLint rule be disabled?

💡 Result:

Yes, the base ESLint "no-use-before-define" rule should be disabled when using the @typescript-eslint/no-use-before-define rule. The typescript-eslint version is an extension rule that replaces the core ESLint rule, adding support for TypeScript features like type, interface, and enum declarations. The base rule can report incorrect errors in TypeScript code, so it must be turned off. Official configuration example from typescript-eslint docs (Flat Config): rules: { "no-use-before-define": "off", "@typescript-eslint/no-use-before-define": "error" } This prevents conflicts and ensures correct TypeScript-aware linting.

Citations:


Disable the base no-use-before-define rule to prevent conflicts with the TypeScript extension rule.

The @typescript-eslint/no-use-before-define rule is a TypeScript-aware replacement for the base ESLint rule. Having both enabled with different variables settings (false for TS rule, true for base) can cause incorrect errors in TypeScript code. According to typescript-eslint documentation, the base rule must be disabled.

Suggested fix
+      "no-use-before-define": "off",
       "@typescript-eslint/no-use-before-define": ["error", {
         variables: false,
         typedefs: false,
         functions: false,
         classes: false,
       }],
-
-      "no-use-before-define": ["error", {
-        variables: true,
-        functions: false,
-        classes: false,
-      }],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@typescript-eslint/no-use-before-define": ["error", {
variables: false,
typedefs: false,
functions: false,
classes: false,
}],
"no-use-before-define": ["error", {
variables: true,
functions: false,
classes: false,
}],
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": ["error", {
variables: false,
typedefs: false,
functions: false,
classes: false,
}],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@eslint.config.mjs` around lines 169 - 180, The base ESLint rule
"no-use-before-define" conflicts with the TypeScript-aware rule
"@typescript-eslint/no-use-before-define" (different `variables` settings), so
disable the base rule to avoid false positives: update the ESLint config to turn
off "no-use-before-define" (or remove it) and keep only
"@typescript-eslint/no-use-before-define" with the existing options; target the
rule key "no-use-before-define" in the config for the change.

Comment thread package.json
The base ESLint rule can produce false positives on TypeScript code.
Disable it and rely solely on @typescript-eslint/no-use-before-define.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant