Skip to content

ESLint v10 support#3230

Open
rasmi wants to merge 4 commits into
import-js:mainfrom
rasmi:eslint-update
Open

ESLint v10 support#3230
rasmi wants to merge 4 commits into
import-js:mainfrom
rasmi:eslint-update

Conversation

@rasmi
Copy link
Copy Markdown
Contributor

@rasmi rasmi commented Feb 19, 2026

Summary

Add ESLint v10 support while maintaining backward compatibility with ESLint v2–v9. The v10 compat changes in shipped code use feature detection (e.g. checking if context.languageOptions exists) rather than version checks.

Closes #3227 (ESLint v10 support) (and maybe #3079).

Split out into separate PRs (independent / pre-existing issues revealed during v10 update; this branch rebases onto them and becomes v10-only):

What changed

Removed API replacements

ESLint v10 removed several deprecated context properties and SourceCode methods. Each call site now feature-detects the new API and falls back to the old one:

  • context.parserOptionscontext.languageOptions.parserOptions (affects sourceType.js, childContext.js, typescript.js, scc.js, parse.js)
  • context.parserPathcontext.languageOptions used for cache hashing instead (scc.js)
  • context.getPhysicalFilename()context.physicalFilename property (contextCompat.js)
  • context.getScope() → already had a compat wrapper, but one call site in declaredScope.js bypassed it — now fixed
  • sourceCode.getTokenOrCommentAfter/Before()sourceCode.getTokenAfter/Before(node, { includeComments: true }) (order.js)

no-unused-modules file enumeration fallback

ESLint v10 removed FileEnumerator and the internal glob-utils module. The existing cascade (FileEnumerator → legacy glob-utils) now has a third tier: a Node.js fs + minimatch fallback that walks directories and matches globs.

  • Only reached when both prior methods throw MODULE_NOT_FOUND or ERR_PACKAGE_PATH_NOT_EXPORTED
  • Uses minimatch and is-glob (existing direct dependencies)
  • On v10, this resolves #3079 (flat config requiring a dummy .eslintrc) since FileEnumerator is no longer used. The issue remains on v9.

Test infrastructure (test files only)

On v10, the unmaintained babel-eslint (Babel 6) is replaced with @babel/eslint-parser v8 for test coverage. This restores ~475 babel-related tests that would otherwise be skipped on v10.

Dependency changes

Package Change Why
eslint || ^10 added to peerDeps and devDeps v10 support

v10-specific parser deps are installed via dep-time-travel.sh (not in devDeps) to avoid peer dep conflicts with typescript@4.5.5 during npm install:

  • @typescript-eslint/parser@8 — on all ESLint 10 Node versions
    -@angular-eslint/template-parser@21 — on all ESLint 10 Node versions (v13 does not support ESLint 10)
  • @babel/eslint-parser@8 + @babel/core@8 (ESM, RC) — on Node >= 20
  • No babel parser on Node 18/19/21 (v8 is ESM-only, engines ^20.19.0 || >=22.12.0; v7 will not get ESLint 10 support). Babel tests are skipped; all other tests run.

CI changes

  • eslint-8+.yml: added ESLint 10 to the matrix, excluded Node < 18
  • dep-time-travel.sh: installs v10-specific parser deps (@typescript-eslint/parser@8 and @angular-eslint/template-parser@21 on all nodes, @babel/eslint-parser@8 on Node >= 20 only)
  • See also Fix resolve install in old npm version on CI. #3237 to fix tests failing in main.

@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 19, 2026

Codecov Report

❌ Patch coverage is 26.76056% with 52 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.87%. Comparing base (395dfc9) to head (e66a4a6).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
src/rules/no-unused-modules.js 2.00% 49 Missing ⚠️
src/rules/order.js 80.00% 2 Missing ⚠️
utils/contextCompat.js 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3230      +/-   ##
==========================================
- Coverage   95.50%   93.87%   -1.64%     
==========================================
  Files          83       83              
  Lines        3693     3755      +62     
  Branches     1336     1355      +19     
==========================================
- Hits         3527     3525       -2     
- Misses        166      230      +64     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@StephanTLavavej
Copy link
Copy Markdown

@rasmi Do you want to disclose your usage of LLMs to generate this PR description and/or PR, which @ljharb objected to in another repo jsx-eslint/eslint-plugin-jsx-a11y#1077 ?

@rasmi
Copy link
Copy Markdown
Contributor Author

rasmi commented Feb 19, 2026

Was not aware, thanks @StephanTLavavej! It is a bit disappointing, as I spent a few hours of my day working on this, even with LLM tools. I will close this PR regardless out of respect for the maintainer (I just want to make sure the tests pass for my own edification).

@rasmi rasmi force-pushed the eslint-update branch 3 times, most recently from efa4b8b to 069a70f Compare February 19, 2026 02:07
@rasmi rasmi closed this Feb 19, 2026
@dirkluijk
Copy link
Copy Markdown

dirkluijk commented Feb 19, 2026

Just curious (and with all respect), what is the real issue of LLM-generated code? What matters is the code, right?

If the PR is too big, or too hard to review, we could just improve that?

@ljharb
Copy link
Copy Markdown
Member

ljharb commented Feb 19, 2026

@StephanTLavavej please do not take it upon yourself to act as if you're a maintainer. Each project can have different policies regardless of who's maintaining them. Please do not attempt to police projects you aren't even a significant contributor to.

@rasmi if you're using an LLM to automate applying changes you're writing, I'm fine with that - however if you're just prompting one to "add eslint v10 support", i have my own LLM subscription for that kind of thing :-)

@dirkluijk no, "what matters" has never been, and never will be, just "the code". Any code contribution represents a legal assignment of rights, as well as an eternal maintenance burden on that code, and as such, the human contributor is an important factor.

@rasmi
Copy link
Copy Markdown
Contributor Author

rasmi commented Feb 19, 2026

@ljharb -- point well-taken! I did go through a more rigorous process here -- it really did take me hours of focused work, thinking about implementation approaches, iterating, fixing tests, etc. It is not my intention to submit slop, I wanted to submit a PR that I hoped would meet the standard of the library, at least as a first pass. I lack decision-making insight/context on things like "Is it okay to just use glob, if not let's do a custom file search" and "exactly how should node vs. eslint-parser vs. core library version compatibility be handled" -- but this is what the review process is for!

I also understand that big changes like this may be simpler for the maintainer to just take on directly, as you have all the proper context and expertise on all the nuances of the library and ecosystem, to say the least. Apologies again, I didn't mean to burden you at all, I know it is hard to maintain core libraries like this. Please do with this what you wish!

Comment thread tests/src/cli.js Outdated
beforeEach(function () {
if (semver.satisfies(eslintPkg.version, '< 6')) {
if (semver.satisfies(eslintPkg.version, '< 6') || semver.satisfies(eslintPkg.version, '>= 10')) {
// TODO: re-enable when eslint-plugin-json supports v10 (https://github.com/azeemba/eslint-plugin-json/issues/97)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor

@fernandopasik fernandopasik Feb 23, 2026

Choose a reason for hiding this comment

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

good idea, last commit in https://github.com/azeemba/eslint-plugin-json was 17 months ago

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Defer to @ljharb.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm fine with that switch, in its own commit.

Copy link
Copy Markdown
Contributor Author

@rasmi rasmi May 9, 2026

Choose a reason for hiding this comment

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

Done in ed9fd92!

Copy link
Copy Markdown
Contributor Author

@rasmi rasmi May 9, 2026

Choose a reason for hiding this comment

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

(Failing on older versions, will fix this eve) fixed!

@A-Loot
Copy link
Copy Markdown

A-Loot commented Mar 7, 2026

What is the current status of this PR?

@rasmi
Copy link
Copy Markdown
Contributor Author

rasmi commented Mar 11, 2026

Hi @ljharb -- let me know if you have any feedback / next steps on this that I can address. Sorry to ping you, but there is quite a crowd on this PR and #3227. If the issue is codecoverage, I can add new test coverage. I think my main concern was whether some of the design decisions are appropriate for the project. (just rebasing onto main & force-pushing now)

- 9
- 8
exclude:
- node-version: 17
Copy link
Copy Markdown
Contributor

@fernandopasik fernandopasik Mar 11, 2026

Choose a reason for hiding this comment

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

probably need to exclude node 18 as well since eslint 10 supports from node 20
https://github.com/eslint/eslint/releases/tag/v10.0.0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The rest of the test suite still runs and passes on Node 18 & 19 + eslint 10, so I left these in, but defer to @ljharb! See e68ea07, it's just the @babel/eslint-parser-dependent ones that are skipped.

@rasmi
Copy link
Copy Markdown
Contributor Author

rasmi commented Mar 12, 2026

#3237 should fix the tests currently failing in main.

Comment thread tests/src/utils.js Outdated
@lorand-horvath
Copy link
Copy Markdown

@rasmi Any progress?

@li-jia-nan
Copy link
Copy Markdown

Are there any updates ?

@rasmi rasmi force-pushed the eslint-update branch from 1664b21 to 7251d47 Compare May 9, 2026 14:19
@socket-security
Copy link
Copy Markdown

socket-security Bot commented May 9, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​eslint-plugin-jsonc@​2.21.19910010086100

View full report

@rasmi
Copy link
Copy Markdown
Contributor Author

rasmi commented May 9, 2026

@rasmi i've got some feedback waiting for you :-)

Sincere apologies for the delay, can commit to a 1-day SLO on feedback/revisions until this is merged!

@rasmi rasmi force-pushed the eslint-update branch 3 times, most recently from 8e2f0e7 to ed9fd92 Compare May 10, 2026 05:26
@rcantin-w
Copy link
Copy Markdown

Thank you for working on this! Just realised I'd have to lose my clever ordering, but I'll suffer the buggy linting until this gets merged 🤞

@Julien-Marcou
Copy link
Copy Markdown

@rasmi I think you need to re-ask @ljharb for a review if your PR is ready

Copy link
Copy Markdown
Member

@ljharb ljharb left a comment

Choose a reason for hiding this comment

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

I've rebased this. Can you pull out the pre-existing bug fixes into their own commits (ideally stacked before the eslint 10 changes)? Would also be nice to extract out the parsers refactors, including typescriptEslintParserSatisfies, to separate commits.

Also, can you elaborate on the eslint-plugin-jsonc stuff? I do see that eslint-plugin-json doesn't work on eslint 10 yet.

}));
return;
case 'ExportAllDeclaration':
exportMap.namespace.set(specifier.exported.name || specifier.exported.value, namespace.add(exportMeta, specifier.source.value));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this means it's not longer doing namespace.add - can you elaborate on this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

namespace.add was a no-op here -- it keys off identifier.name, but was passed specifier.source.value (the module string), so it never set a getter, just returned the bare {}. Updated code sets the lazy getter directly, same as ExportNamespaceSpecifier, so export * as ns resolves.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Moved to #3250

Comment thread src/rules/no-unused-modules.js Outdated
// Extract the base directory from the glob (everything before the first glob character)
const base = pattern.replace(/[*?{[].*/g, '').replace(/\/[^/]*$/, '') || '.';
const resolvedBase = resolve(base);
const allFiles = walkDirectory(resolvedBase, extensions, [], fs, join, extname);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can use glob if you use a version that's old enough to support the same node versions as us.

* @param {string[]} extensions - list of supported file extensions
* @returns {string[]} list of matched file paths
*/
function listFilesWithNodeFs(src, extensions) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why wouldn't this function work on eslint 9?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It would work, just not reached as a fallback. Updated comment to clarify!

Comment thread tests/src/rules/named.js

test({
code: 'const { baz } = require("./bar")',
errors: [error('baz', './bar')],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why are these two no longer errors?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the fix from earlier. These never errored -- checkRequire early-returns unless commonjs: true, which these don't set. The errors: was presumably copied from the adjacent case in 54d86c8. Previous RuleTester just ignored it, it now flags it which is why the test failed / required a workaround in the earlier commit/comment. Will split this out in the bugfix commit!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Moved to #3251

}),
_test({
code: 'import("../" + name)',
errors: [dynamicImportError],
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

and why are these no longer errors?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same issue as above! no-dynamic-require only flags import() with esmodule: true, which these don't set, so they never errored. These were added in 7163824 and the errors was ignored by the previous RuleTester.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

(can move these into a fix commit as well)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Moved to #3251

Comment thread package.json
"eslint-plugin-import": "2.x",
"eslint-plugin-json": "^2.1.2",
"eslint-plugin-jsonc": "^2.21.1",
"espree": "^9",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why is this pin needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

espree is normally hoisted transitively from eslint, but installing eslint-plugin-jsonc@^3 for v10 via dep-time-travel.sh de-hoists it, breaking require.resolve('espree') in utils.js (see failed test run here). I used ^9 to avoid v11, which uses Array.prototype.at() and dies on node 12/14 (node 12 failure, node failure). v9 still works for the tests that use parsers.ESPREE.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These become unnecessary if we use a fixture as in #3252

@rasmi
Copy link
Copy Markdown
Contributor Author

rasmi commented May 18, 2026

@ljharb -- ready!

The jsonc stuff is messy, we could use a fixture (see #3252) or skip the cli.js test for #1645 for v10+ (not ideal), or do these version-pinning workarounds (or cleaner workarounds if you have any ideas). The context for the jsonc switch (originally proposed here) is: v2 works on eslint 6-9, but eslint 10 needs v3 (ESM-only, newer Node). No single version covers every eslint, so v3 is only installed for the eslint-10 runs in dep-time-travel.sh.

I kept eslint-plugin-json for the old .eslintrc.json path instead of switching everything to jsonc. jsonc v2 pulls in synckit, which uses syntax Node 12 can't parse (failed run). no-unused-modules loads that .eslintrc.json, so switching it would break unrelated tests on Node 12. The flat-config path (eslint 9+) uses jsonc, and cli.js asserts differently depending on which one is active.

If we can use a fixture to reproduce the issue in #1645 instead of eslint-plugin-json or eslint-plugin-jsonc directly, these workarounds can be avoided entirely (see #3252).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet