fix(linter): multi-version support compliance for @nx/eslint and @nx/eslint-plugin#35811
Merged
Conversation
✅ Deploy Preview for nx-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for nx-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Contributor
|
View your CI Pipeline Execution ↗ for commit 12c53d0
☁️ Nx Cloud last updated this comment at |
|
No dependency changes detected. Learn more about Socket for GitHub. 👍 No dependency changes detected in pull request |
jaysoo
approved these changes
May 29, 2026
…fresh installs to v9
Restructure `packages/eslint/src/utils/versions.ts` into the canonical
versionMap bundle pattern: top-level `eslintVersion` and
`typescriptESLintVersion` hold the latest supported (^9.8.0 / ^8.40.0);
the v8 lane lives in `versionMap[8]`. Add `minSupportedEslintVersion`
and a 7-line `assert-supported-eslint-version.ts` wrapper around
`@nx/devkit/internal`'s `assertSupportedPackageVersion`. Move
`getInstalledEslintVersion` / `getInstalledEslintMajorVersion` here per
the cypress canonical pattern.
Drop the local re-implementation of `getInstalledPackageVersion` in
`version-utils.ts` in favor of `@nx/devkit/internal`'s shared
`getInstalledPackageVersion` / `getDeclaredPackageVersion` helpers,
which centralize dist-tag handling and pnpm catalog resolution.
Route every consumer through `versions(tree)`: `init`, `init-migration`,
`setup-root-eslint`, `convert-to-flat-config`, `workspace-rule`, and
the `update-typescript-eslint-v8-13-0` migration. The bare
`@nx/eslint:init` path now lands fresh installs on `^9.8.0` instead of
the previous `~8.57.0`, aligning with the flat-config path that already
shipped v9. Installed versions are respected via the version map; v10
falls through to `latestVersions` (no above-ceiling throw).
Replace `gte(eslintVersion, '9.0.0')` in `eslint-file.ts` with a
numeric major comparison (`getInstalledEslintMajorVersion(tree) ?? 9`).
The previous code would have crashed if the fallback path ever fired
(`gte('^9.8.0', ...)` throws on the range).
Switch the `addExtendsToLintConfig` specs from mocking
`readModulePackageJson` to declaring `eslint` in the tree's
`package.json`, matching the canonical generator-time read pattern.
Rename `@nx/eslint/internal`'s `eslint9__typescriptESLintVersion`
export to `typescriptESLintVersion`, and update the lone external
consumer in `@nx/angular`'s `add-angular-eslint-dependencies.ts`.
Call `assertSupportedEslintVersion(tree)` as the first statement of every working function backing a `generators.json` entry — `initEsLint`, `lintWorkspaceRulesProjectGenerator`, `lintWorkspaceRuleGenerator`, `convertToFlatConfigGenerator`, and `convertToInferred`. Workspaces declaring `eslint` below the supported floor (`8.0.0`) now fail fast with the canonical `Unsupported version of` error before any tree mutations, instead of silently writing configs incompatible with the installed ESLint major. The `lintInitGenerator` wrapper does not assert itself — it delegates to `initEsLint`, which carries the assert (canonical wrapper/internal-split pattern).
…enerators Flip the default of `keepExistingVersions` to `true` on `@nx/eslint:init`'s schema, and pass `true` (or `keepExistingVersions ?? true` where the option is propagated from a caller) to every generator-side `addDependenciesToPackageJson` call — `setup-root-eslint` legacy and flat-config paths, the `convert-to-flat-config` post-conversion dependency block, `workspace-rule`, `workspace-rules-project`, and `init-migration`'s `@eslint/js` and `@nx/eslint-plugin` writes. Generators that previously overwrote installed ESLint, typescript-eslint, and related dependency pins on re-run now leave user-pinned versions intact. Bumping remains the responsibility of migrations, which are explicitly exempt and not modified.
…SLint 8
Replace the ad-hoc `Number(version[0]) < 7 || ...` floor check and
inlined `ESLint must be version 7.6 or higher.` throw in
`packages/eslint/src/executors/lint/lint.impl.ts` with the shared
`assertSupportedInstalledPackageVersion('eslint', minSupportedEslintVersion)`
helper from `@nx/devkit/internal`. The floor moves from the stale
`7.6` to `8.0.0` (matching the declared `eslint` peer range), and the
thrown message now matches the canonical
`Unsupported version of \`eslint\` detected` format used across plugin
floor asserts.
Update the executor spec to mock `readModulePackageJson` (the FS read
backing `getInstalledPackageVersion`) for the sub-floor case via
`mockReturnValueOnce`, so the mock applies only to the targeted test
and other tests run against the actual installed `eslint`.
Add `requires` blocks to two migration generators in
`packages/eslint/migrations.json` so they only run on workspaces where
their target package is actually present at a relevant version:
* `update-typescript-eslint-v8.13.0` is now gated on
`@typescript-eslint/parser: >=8.0.0`. Workspaces below v8 are skipped
by the migrate runner instead of running the migration body and
silently no-opping. The migration body keeps its per-package
`gte('8.0.0') && lt('8.13.0')` guard, which covers the
lockstep-broken case where one of the four typescript-eslint packages
drifts to a different major than the parser; a comment in the
migration documents the split.
* `add-file-extensions-to-overrides` is now gated on
`eslint: >=8.57.0`. Workspaces without ESLint (or below the
flat-config-compat baseline) no longer run the migration body.
Both gates are intentionally open at the upper bound — migration
generators should remain runnable via `nx migrate --from <older>` even
once newer Nx versions ship.
The `eslint-config-prettier` cross-major bump in `packageJsonUpdates`
`20.7.0` is left ungated: v10's `eslint >=7.0.0` peer covers the entire
supported ESLint window, `@nx/eslint-plugin` itself peers
`eslint-config-prettier: ^10.0.0` via the catalog, and there are no
codemods between v8/v9/v10 — the bump is corrective for sub-v10
workspaces, not a footgun.
…s enforce the floor Add `packages/eslint/src/utils/all-generators-enforce-floor.spec.ts` using the shared `assertGeneratorsEnforceVersionFloor` from `@nx/devkit/internal-testing-utils`. The spec iterates every entry in `@nx/eslint`'s `generators.json` and verifies it throws the canonical `Unsupported version of` error on a sub-floor (`~7.32.0`) workspace. This pins the floor-assert behavior at the suite level, so a future generator added to `generators.json` without an assert in its working function fails this spec — matching the canonical shape now used by `@nx/cypress`, `@nx/playwright`, `@nx/angular`, `@nx/js`, and `@nx/rsbuild`.
… to ^8 Restrict the `@typescript-eslint/parser` peer in `packages/eslint-plugin/package.json` from `^6.13.2 || ^7.0.0 || ^8.0.0` to `^8.0.0` so the declared peer matches the plugin's actually-bundled runtime dependencies. The plugin ships `@typescript-eslint/utils` and `@typescript-eslint/type-utils` pinned to `^8.0.0` via the `catalog:eslint` entry, and workspaces previously on `@typescript-eslint/parser@^6` or `^7` silently ran the plugin's rules against v8 internals — risking AST-shape drift on rules that walk TSESTree nodes. The plugin does not directly invoke `@typescript-eslint/parser` (only the user's ESLint config does, when configured to parse TypeScript files), so the peer is declared as optional via `peerDependenciesMeta`. Users who only lint JavaScript no longer see an unmet-peer warning. This matches the canonical optional-peer pattern used by `@playwright/test`, `cypress`, `vitest`, etc. `@typescript-eslint/utils` and `@typescript-eslint/type-utils` remain in `dependencies` — the plugin self-contains its runtime utils rather than peering on the user's installation.
… to ^8 Restrict the `@typescript-eslint/parser` peer in `packages/eslint-plugin/package.json` from `^6.13.2 || ^7.0.0 || ^8.0.0` to `^8.0.0` so the declared peer matches the plugin's actually-bundled runtime dependencies. The plugin ships `@typescript-eslint/utils` and `@typescript-eslint/type-utils` pinned to `^8.0.0` via the `catalog:eslint` entry, and workspaces previously on `@typescript-eslint/parser@^6` or `^7` silently ran the plugin's rules against v8 internals — risking AST-shape drift on rules that walk TSESTree nodes. The plugin does not directly invoke `@typescript-eslint/parser` (only the user's ESLint config does, when configured to parse TypeScript files), so the peer is declared as optional via `peerDependenciesMeta`. Users who only lint JavaScript no longer see an unmet-peer warning. This matches the canonical optional-peer pattern used by `@playwright/test`, `cypress`, `vitest`, etc. `@typescript-eslint/utils` and `@typescript-eslint/type-utils` remain in `dependencies` — the plugin self-contains its runtime utils rather than peering on the user's installation.
Honor the user's flat-config preference when picking the fresh-install lane in `packages/eslint/src/utils/versions.ts`. When `useFlatConfig` returns `false` (typically via `ESLINT_USE_FLAT_CONFIG=false`), install the v8 ESLint + typescript-eslint v7 lane so the workspace remains internally consistent: ESLint v9 with eslintrc is supported for runtime linting, but `@typescript-eslint/rule-tester` v8 dropped eslintrc support entirely and v7 rule-tester is pinned to ESLint v8 — so legacy workspaces with a v9 + typescript-eslint v8 stack break the `workspace-rule` generator's emitted spec. `convert-to-flat-config` is an opt-in upgrade: after conversion the workspace is on the v9 flat-config-ready stack, so the generator now writes the latest `eslintVersion` / `typescriptESLintVersion` constants directly rather than routing through `versions(tree)` (which would pick the v8/v7 lane based on pre-conversion eslintrc state) and drops the `keepExistingVersions` flag (this generator is meant to bump existing pre-conversion pins). The `@nx/vue` library snapshot is updated to reflect the v9 flat-config defaults the vue library generator now produces.
…versions(tree) directly
`packages/eslint/src/utils/version-utils.ts` was a two-function file
whose entries (`getInstalledEslintVersion`, `getTypeScriptEslintVersionToInstall`)
were one-line indirections over what `versions.ts` already exposes. Drop
the file and route every consumer through `versions(tree)` directly:
* `packages/eslint/internal.ts` re-exports `getInstalledEslintVersion`,
`typescriptESLintVersion`, and now also `versions` directly from
`./src/utils/versions`.
* `packages/eslint/src/generators/workspace-rules-project/workspace-rules-project.ts`
reads `versions(tree).typescriptESLintVersion` inline.
* `packages/vue/src/utils/add-linting.ts` swaps to the same call shape.
`packages/vue/src/generators/library/library.spec.ts` adds a per-test
`ESLINT_USE_FLAT_CONFIG=true` override for the "should add vue and vitest
to package.json when non-buildable" case. Jest's pnpm hoisting can
resolve `require('eslint')` to a v8 copy in the workspace store, which
would otherwise make `useFlatConfig` route the fresh-install lane
through the v8/v7 stack and not match the v9 flat-config snapshot. The
snapshot is refreshed to capture what real flat-config users actually
see (`@eslint/js`, `typescript-eslint` umbrella package, no
`@typescript-eslint/eslint-plugin`).
No production behavior change.
The legacy install lane (no flat config) paired ESLint v8 with typescript-eslint v7, which pins `@typescript-eslint/parser` to v7. With `@nx/eslint-plugin`'s parser peer tightened to `^8.0.0`, `npm install` fails with ERESOLVE in fresh workspaces that pull in `@nx/eslint-plugin` (e.g. `nx g @nx/remix:app --linter=eslint`). typescript-eslint v8 supports ESLint v8.57+ and still works with eslintrc, so the v8 lane can ship typescript-eslint v8. The only sub-package that dropped eslintrc is `@typescript-eslint/rule-tester`, which `@nx/eslint` only installs in the flat-config lane.
Contributor
There was a problem hiding this comment.
Nx Cloud has identified a flaky task in your failed CI:
🔂 Since the failure was identified as flaky, we triggered a CI rerun by adding an empty commit to this branch.
🎓 Learn more about Self-Healing CI on nx.dev
jaysoo
pushed a commit
that referenced
this pull request
May 29, 2026
…eslint-plugin (#35811) ## Current Behavior `@nx/eslint` and `@nx/eslint-plugin` have several multi-version support compliance gaps: - The bare `@nx/eslint:init` generator path lands fresh installs on EOL ESLint v8 (`~8.57.0`) even though the flat-config path already ships v9. - Local re-implementations of installed-version helpers in `version-utils.ts` and per-major version aliases (`eslint9__*`) in `versions.ts` drift from the shared `@nx/devkit/internal` helpers. - Generators do not assert the supported ESLint floor at their entry points — sub-floor workspaces silently get configs incompatible with their installed ESLint. - `addDependenciesToPackageJson` call sites in generators do not preserve user-pinned ESLint dependency versions; `init`'s schema defaults `keepExistingVersions` to `false`. - The `@nx/eslint:lint` executor enforces a stale `7.6` floor with a hand-rolled `Number(version[0])` check rather than the shared canonical helper. - Two ESLint migration generators (`update-typescript-eslint-v8.13.0`, `add-file-extensions-to-overrides`) lack `requires` gates, so they run on every workspace even when their target packages are absent or at unrelated versions. - `@nx/eslint-plugin` declares `@typescript-eslint/parser: ^6.13.2 || ^7.0.0 || ^8.0.0` as a required peer but ships `@typescript-eslint/utils` / `@typescript-eslint/type-utils` pinned to `^8.0.0` — the peer claim is wider than the bundled runtime range, and users only linting JavaScript see an unmet-peer warning. ## Expected Behavior `@nx/eslint`: - Fresh installs default to ESLint v9 via the canonical `versions(tree)` route. Installed versions are respected through the version map (v8 → `~8.57.0` lane; v9/v10 → `latestVersions`, with v10 silently falling through — no above-ceiling throw). - `versions.ts` follows the bundle pattern; `version-utils.ts` delegates to `@nx/devkit/internal` helpers. - Every `generators.json` entry asserts the supported floor (`8.0.0`) at its working function's first statement via the canonical `assertSupportedEslintVersion(tree)` wrapper. A parameterized `all-generators-enforce-floor.spec.ts` pins this so a future generator added without the assert fails the spec. - All generator-side `addDependenciesToPackageJson` calls preserve user-pinned versions: `init`'s schema defaults `keepExistingVersions` to `true`, and programmatic callers default via `?? true`. - The `@nx/eslint:lint` executor uses the shared `assertSupportedInstalledPackageVersion` helper to enforce the `8.0.0` floor with the canonical `Unsupported version of \`eslint\` detected` message. - The two migration generators are gated on `@typescript-eslint/parser >=8.0.0` and `eslint >=8.57.0` respectively (open upper bound so `nx migrate --from <older>` still applies them). `@nx/eslint-plugin`: - `@typescript-eslint/parser` peer is tightened to `^8.0.0` (matching the bundled `@typescript-eslint/utils` / `type-utils` v8 runtime) and declared optional via `peerDependenciesMeta` — users linting only JavaScript no longer see unmet-peer warnings. ## Implementation Details > [!IMPORTANT] > This PR includes a cherry-picked commit (`feat(devkit): add assertSupportedInstalledPackageVersion to @nx/devkit/internal`) that originates from #35806. Whichever of the two PRs merges first, the other will be rebased to drop the duplicate commit. <!-- polygraph-session-start --> --- [View session information ↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4387-fa5e84db) <!-- polygraph-session-end --> --------- Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com> Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
vrxj81
pushed a commit
to vrxj81/nx
that referenced
this pull request
Jun 7, 2026
…eslint-plugin (nrwl#35811) ## Current Behavior `@nx/eslint` and `@nx/eslint-plugin` have several multi-version support compliance gaps: - The bare `@nx/eslint:init` generator path lands fresh installs on EOL ESLint v8 (`~8.57.0`) even though the flat-config path already ships v9. - Local re-implementations of installed-version helpers in `version-utils.ts` and per-major version aliases (`eslint9__*`) in `versions.ts` drift from the shared `@nx/devkit/internal` helpers. - Generators do not assert the supported ESLint floor at their entry points — sub-floor workspaces silently get configs incompatible with their installed ESLint. - `addDependenciesToPackageJson` call sites in generators do not preserve user-pinned ESLint dependency versions; `init`'s schema defaults `keepExistingVersions` to `false`. - The `@nx/eslint:lint` executor enforces a stale `7.6` floor with a hand-rolled `Number(version[0])` check rather than the shared canonical helper. - Two ESLint migration generators (`update-typescript-eslint-v8.13.0`, `add-file-extensions-to-overrides`) lack `requires` gates, so they run on every workspace even when their target packages are absent or at unrelated versions. - `@nx/eslint-plugin` declares `@typescript-eslint/parser: ^6.13.2 || ^7.0.0 || ^8.0.0` as a required peer but ships `@typescript-eslint/utils` / `@typescript-eslint/type-utils` pinned to `^8.0.0` — the peer claim is wider than the bundled runtime range, and users only linting JavaScript see an unmet-peer warning. ## Expected Behavior `@nx/eslint`: - Fresh installs default to ESLint v9 via the canonical `versions(tree)` route. Installed versions are respected through the version map (v8 → `~8.57.0` lane; v9/v10 → `latestVersions`, with v10 silently falling through — no above-ceiling throw). - `versions.ts` follows the bundle pattern; `version-utils.ts` delegates to `@nx/devkit/internal` helpers. - Every `generators.json` entry asserts the supported floor (`8.0.0`) at its working function's first statement via the canonical `assertSupportedEslintVersion(tree)` wrapper. A parameterized `all-generators-enforce-floor.spec.ts` pins this so a future generator added without the assert fails the spec. - All generator-side `addDependenciesToPackageJson` calls preserve user-pinned versions: `init`'s schema defaults `keepExistingVersions` to `true`, and programmatic callers default via `?? true`. - The `@nx/eslint:lint` executor uses the shared `assertSupportedInstalledPackageVersion` helper to enforce the `8.0.0` floor with the canonical `Unsupported version of \`eslint\` detected` message. - The two migration generators are gated on `@typescript-eslint/parser >=8.0.0` and `eslint >=8.57.0` respectively (open upper bound so `nx migrate --from <older>` still applies them). `@nx/eslint-plugin`: - `@typescript-eslint/parser` peer is tightened to `^8.0.0` (matching the bundled `@typescript-eslint/utils` / `type-utils` v8 runtime) and declared optional via `peerDependenciesMeta` — users linting only JavaScript no longer see unmet-peer warnings. ## Implementation Details > [!IMPORTANT] > This PR includes a cherry-picked commit (`feat(devkit): add assertSupportedInstalledPackageVersion to @nx/devkit/internal`) that originates from nrwl#35806. Whichever of the two PRs merges first, the other will be rebased to drop the duplicate commit. <!-- polygraph-session-start --> --- [View session information ↗](https://snapshot.app.trypolygraph.com/orgs/69cdc268b6aa527e4129c2b4/sessions/nxc-4387-fa5e84db) <!-- polygraph-session-end --> --------- Co-authored-by: Leosvel Pérez Espinosa <leosvel.perez.espinosa@gmail.com> Co-authored-by: nx-cloud[bot] <71083854+nx-cloud[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Current Behavior
@nx/eslintand@nx/eslint-pluginhave several multi-version support compliance gaps:@nx/eslint:initgenerator path lands fresh installs on EOL ESLint v8 (~8.57.0) even though the flat-config path already ships v9.version-utils.tsand per-major version aliases (eslint9__*) inversions.tsdrift from the shared@nx/devkit/internalhelpers.addDependenciesToPackageJsoncall sites in generators do not preserve user-pinned ESLint dependency versions;init's schema defaultskeepExistingVersionstofalse.@nx/eslint:lintexecutor enforces a stale7.6floor with a hand-rolledNumber(version[0])check rather than the shared canonical helper.update-typescript-eslint-v8.13.0,add-file-extensions-to-overrides) lackrequiresgates, so they run on every workspace even when their target packages are absent or at unrelated versions.@nx/eslint-plugindeclares@typescript-eslint/parser: ^6.13.2 || ^7.0.0 || ^8.0.0as a required peer but ships@typescript-eslint/utils/@typescript-eslint/type-utilspinned to^8.0.0— the peer claim is wider than the bundled runtime range, and users only linting JavaScript see an unmet-peer warning.Expected Behavior
@nx/eslint:versions(tree)route. Installed versions are respected through the version map (v8 →~8.57.0lane; v9/v10 →latestVersions, with v10 silently falling through — no above-ceiling throw).versions.tsfollows the bundle pattern;version-utils.tsdelegates to@nx/devkit/internalhelpers.generators.jsonentry asserts the supported floor (8.0.0) at its working function's first statement via the canonicalassertSupportedEslintVersion(tree)wrapper. A parameterizedall-generators-enforce-floor.spec.tspins this so a future generator added without the assert fails the spec.addDependenciesToPackageJsoncalls preserve user-pinned versions:init's schema defaultskeepExistingVersionstotrue, and programmatic callers default via?? true.@nx/eslint:lintexecutor uses the sharedassertSupportedInstalledPackageVersionhelper to enforce the8.0.0floor with the canonicalUnsupported version of \eslint` detected` message.@typescript-eslint/parser >=8.0.0andeslint >=8.57.0respectively (open upper bound sonx migrate --from <older>still applies them).@nx/eslint-plugin:@typescript-eslint/parserpeer is tightened to^8.0.0(matching the bundled@typescript-eslint/utils/type-utilsv8 runtime) and declared optional viapeerDependenciesMeta— users linting only JavaScript no longer see unmet-peer warnings.Implementation Details
Important
This PR includes a cherry-picked commit (
feat(devkit): add assertSupportedInstalledPackageVersion to @nx/devkit/internal) that originates from #35806. Whichever of the two PRs merges first, the other will be rebased to drop the duplicate commit.View session information ↗