@nx/eslint 23.0.0 → 23.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +1 -2
  2. package/dist/src/executors/lint/lint.impl.js +0 -5
  3. package/dist/src/executors/lint/schema.d.ts +1 -14
  4. package/dist/src/executors/lint/schema.json +1 -15
  5. package/dist/src/executors/lint/utility/eslint-utils.js +7 -5
  6. package/dist/src/generators/convert-to-flat-config/generator.js +42 -19
  7. package/dist/src/generators/convert-to-flat-config/schema.d.ts +3 -0
  8. package/dist/src/generators/convert-to-flat-config/schema.json +6 -0
  9. package/dist/src/generators/init/global-eslint-config.d.ts +1 -1
  10. package/dist/src/generators/utils/eslint-file.js +20 -28
  11. package/dist/src/generators/workspace-rule/files/__name__.spec.ts__tmpl__ +1 -7
  12. package/dist/src/generators/workspace-rule/workspace-rule.js +4 -15
  13. package/dist/src/migrations/update-23-1-0/convert-to-flat-config.d.ts +15 -0
  14. package/dist/src/migrations/update-23-1-0/convert-to-flat-config.js +283 -0
  15. package/dist/src/migrations/update-23-1-0/convert-to-flat-config.md +286 -0
  16. package/dist/src/plugins/plugin.js +8 -11
  17. package/dist/src/utils/assert-supported-eslint-version.js +0 -4
  18. package/dist/src/utils/config-file.js +1 -8
  19. package/dist/src/utils/deprecation.d.ts +0 -2
  20. package/dist/src/utils/deprecation.js +1 -11
  21. package/dist/src/utils/flat-config.d.ts +1 -0
  22. package/dist/src/utils/flat-config.js +20 -18
  23. package/dist/src/utils/resolve-eslint-class.js +11 -17
  24. package/dist/src/utils/versions.d.ts +3 -4
  25. package/dist/src/utils/versions.js +5 -16
  26. package/migrations.json +121 -0
  27. package/package.json +7 -7
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = update;
4
+ const devkit_1 = require("@nx/devkit");
5
+ const config_file_1 = require("../../utils/config-file");
6
+ const generator_1 = require("../../generators/convert-to-flat-config/generator");
7
+ // Output formatters ESLint removed in v9. Built-in names only; community
8
+ // formatter packages (referenced by their package name) keep working.
9
+ const REMOVED_FORMATTERS = new Set([
10
+ 'compact',
11
+ 'codeframe',
12
+ 'unix',
13
+ 'visualstudio',
14
+ 'table',
15
+ 'checkstyle',
16
+ 'jslint-xml',
17
+ 'junit',
18
+ 'tap',
19
+ ]);
20
+ // Executor options the flat-config lint executor rejects outright (it throws when
21
+ // any is present). The generator folds project-level `ignorePath` into the flat
22
+ // config `ignores`, so that one is reported only when inherited from targetDefaults.
23
+ const FLAT_CONFIG_UNSUPPORTED_OPTIONS = [
24
+ 'ignorePath',
25
+ 'resolvePluginsRelativeTo',
26
+ 'reportUnusedDisableDirectives',
27
+ ];
28
+ const ROOT_ESLINTRC_CANDIDATES = [
29
+ '.eslintrc.base.json',
30
+ '.eslintrc',
31
+ '.eslintrc.json',
32
+ '.eslintrc.yaml',
33
+ '.eslintrc.yml',
34
+ ];
35
+ const ESLINT_LINT_EXECUTOR = '@nx/eslint:lint';
36
+ /**
37
+ * Hybrid migration paired with `convert-to-flat-config.md`. The deterministic
38
+ * half reuses the `@nx/eslint:convert-to-flat-config` generator to convert
39
+ * JSON/YAML eslintrc configs to flat config (the version bump is owned by
40
+ * `packageJsonUpdates`, so it runs with `keepExistingVersions`). It then returns
41
+ * `agentContext` describing the work the generator could not do deterministically
42
+ * (JavaScript-based configs, removed output formatters, the passing-state
43
+ * baseline) so the paired prompt's agent can finish the job and keep the
44
+ * workspace lint-passing.
45
+ */
46
+ async function update(tree) {
47
+ // Gather pre-conversion context: the generator deletes the eslintrc files it
48
+ // converts, so anything derived from them must be captured first.
49
+ const userExplicitRules = collectUserRuleIds(tree);
50
+ const skippedJsConfigs = findJsProjectConfigs(tree);
51
+ const removedFormatterTargets = findRemovedFormatterTargets(tree);
52
+ const unsupportedOptionTargets = findUnsupportedFlatConfigOptionTargets(tree);
53
+ const rootState = detectRootConfigState(tree);
54
+ if (rootState === 'none') {
55
+ // No ESLint configuration to migrate.
56
+ return;
57
+ }
58
+ if (rootState === 'convertible') {
59
+ await (0, generator_1.convertToFlatConfigGenerator)(tree, {
60
+ keepExistingVersions: true,
61
+ skipFormat: false,
62
+ });
63
+ }
64
+ const agentContext = [
65
+ passingBaselineInstruction(userExplicitRules),
66
+ ];
67
+ const nextSteps = [];
68
+ if (rootState === 'js') {
69
+ agentContext.push('The root ESLint config is JavaScript-based (.eslintrc.js or .eslintrc.cjs) and was not converted automatically. ' +
70
+ 'Convert the whole workspace to flat config by hand: produce an eslint.config.mjs at the root and one per project, ' +
71
+ 'preserving the existing rules, plugins, parser options and overrides.');
72
+ nextSteps.push('The root ESLint config is JavaScript-based and must be converted to flat config manually (root and every project).');
73
+ }
74
+ if (skippedJsConfigs.length > 0) {
75
+ agentContext.push(`These project ESLint configs are JavaScript-based and were not converted automatically: ${skippedJsConfigs.join(', ')}. Convert each one to a flat config (eslint.config.mjs) manually, mirroring the conversion applied to the JSON/YAML configs.`);
76
+ nextSteps.push(`Convert these JavaScript-based ESLint configs to flat config manually: ${skippedJsConfigs.join(', ')}.`);
77
+ }
78
+ if (removedFormatterTargets.length > 0) {
79
+ agentContext.push(`These lint targets use an ESLint output formatter that was removed in v9: ${removedFormatterTargets.join('; ')}. Switch each to a built-in formatter (stylish, html, json, json-with-metadata) or install the matching community package (for example eslint-formatter-junit) and reference it by its package name.`);
80
+ nextSteps.push(`Update lint targets that use a removed ESLint formatter: ${removedFormatterTargets.join('; ')}.`);
81
+ }
82
+ if (unsupportedOptionTargets.length > 0) {
83
+ agentContext.push(`These lint targets set an ESLint option that flat config no longer supports, so the flat-config executor will throw: ${unsupportedOptionTargets.join('; ')}. Remove each option from its target or nx.json targetDefaults and migrate the behavior into the flat config where it applies: fold ignorePath patterns into the ignores block, set reportUnusedDisableDirectives via linterOptions. resolvePluginsRelativeTo has no flat-config equivalent. The workspace must still lint cleanly afterward.`);
84
+ nextSteps.push(`Remove ESLint executor options that flat config no longer supports: ${unsupportedOptionTargets.join('; ')}.`);
85
+ }
86
+ if (rootState === 'convertible' && generatedConfigsUseFlatCompat(tree)) {
87
+ agentContext.push('One or more generated flat configs use the FlatCompat shim from the @eslint/eslintrc package for third-party "extends" or complex overrides. ' +
88
+ 'Convert each FlatCompat usage to flat-native config when it is low-risk (for example typescript-eslint flat configs, or plugins that ship flat presets); otherwise keep the shim. ' +
89
+ 'The workspace must still lint cleanly afterward.');
90
+ }
91
+ return { agentContext, nextSteps };
92
+ }
93
+ function detectRootConfigState(tree) {
94
+ const hasFlatConfig = [
95
+ ...config_file_1.ESLINT_FLAT_CONFIG_FILENAMES,
96
+ ...config_file_1.BASE_ESLINT_CONFIG_FILENAMES,
97
+ ].some((file) => tree.exists(file));
98
+ if (hasFlatConfig) {
99
+ return 'flat';
100
+ }
101
+ if (tree.exists('.eslintrc.js') || tree.exists('.eslintrc.cjs')) {
102
+ return 'js';
103
+ }
104
+ if (ROOT_ESLINTRC_CANDIDATES.some((file) => tree.exists(file))) {
105
+ return 'convertible';
106
+ }
107
+ return 'none';
108
+ }
109
+ // Collects every rule ID the user explicitly configured across all eslintrc
110
+ // layers (root, base and per-project), so the agent can tell user-chosen rules
111
+ // apart from preset defaults when restoring the passing baseline. JavaScript
112
+ // configs are unreadable here and are surfaced separately.
113
+ function collectUserRuleIds(tree) {
114
+ const ruleIds = new Set();
115
+ const roots = ['', ...[...(0, devkit_1.getProjects)(tree).values()].map((p) => p.root)];
116
+ for (const root of roots) {
117
+ for (const filename of ROOT_ESLINTRC_CANDIDATES) {
118
+ const path = root ? `${root}/${filename}` : filename;
119
+ if (!tree.exists(path)) {
120
+ continue;
121
+ }
122
+ const config = readEslintrcConfig(tree, path);
123
+ if (!config) {
124
+ continue;
125
+ }
126
+ for (const id of Object.keys(config.rules ?? {})) {
127
+ ruleIds.add(id);
128
+ }
129
+ for (const override of config.overrides ?? []) {
130
+ for (const id of Object.keys(override?.rules ?? {})) {
131
+ ruleIds.add(id);
132
+ }
133
+ }
134
+ }
135
+ }
136
+ return [...ruleIds].sort();
137
+ }
138
+ function findJsProjectConfigs(tree) {
139
+ const configs = [];
140
+ for (const [, projectConfig] of (0, devkit_1.getProjects)(tree)) {
141
+ for (const filename of ['.eslintrc.js', '.eslintrc.cjs']) {
142
+ const path = `${projectConfig.root}/${filename}`;
143
+ if (tree.exists(path)) {
144
+ configs.push(path);
145
+ }
146
+ }
147
+ }
148
+ return configs;
149
+ }
150
+ function findRemovedFormatterTargets(tree) {
151
+ const targets = [];
152
+ // Emits one entry per option set (default + each configuration) whose `format`
153
+ // names a formatter ESLint removed in v9.
154
+ const collectRemovedFormatters = (baseLabel, optionSets) => {
155
+ for (const [configuration, options] of optionSets) {
156
+ const format = options?.format;
157
+ if (typeof format === 'string' && REMOVED_FORMATTERS.has(format)) {
158
+ const label = configuration
159
+ ? `${baseLabel}:${configuration}`
160
+ : baseLabel;
161
+ targets.push(`${label} (format: "${format}")`);
162
+ }
163
+ }
164
+ };
165
+ for (const [project, projectConfig] of (0, devkit_1.getProjects)(tree)) {
166
+ for (const [targetName, target] of Object.entries(projectConfig.targets ?? {})) {
167
+ // Scan both the default options and every configuration, since a removed
168
+ // formatter is often only set on a CI-specific configuration.
169
+ const optionSets = [
170
+ [null, target.options],
171
+ ...Object.entries(target.configurations ?? {}),
172
+ ];
173
+ collectRemovedFormatters(`${project}:${targetName}`, optionSets);
174
+ }
175
+ }
176
+ // Lint options are commonly centralized in nx.json targetDefaults; a removed
177
+ // formatter set there is inherited by every lint target and would be missed by
178
+ // the per-project scan above (getProjects does not merge targetDefaults).
179
+ const targetDefaults = (0, devkit_1.readNxJson)(tree)?.targetDefaults ?? {};
180
+ for (const [name, target] of Object.entries(targetDefaults)) {
181
+ if (name !== 'lint' && name !== ESLINT_LINT_EXECUTOR) {
182
+ continue;
183
+ }
184
+ const optionSets = [[null, target.options], ...Object.entries(target.configurations ?? {})];
185
+ collectRemovedFormatters(`targetDefaults["${name}"]`, optionSets);
186
+ }
187
+ return targets;
188
+ }
189
+ // Finds lint executor options that flat config rejects and that survive the
190
+ // conversion, so they can be surfaced for manual cleanup. The flat-config executor
191
+ // throws on these, so an inherited one fails every affected lint target.
192
+ function findUnsupportedFlatConfigOptionTargets(tree) {
193
+ const entries = [];
194
+ const collect = (baseLabel, optionSets, candidates) => {
195
+ for (const [configuration, options] of optionSets) {
196
+ for (const option of candidates) {
197
+ const value = options?.[option];
198
+ // reportUnusedDisableDirectives only throws when truthy; the others throw
199
+ // whenever they are set, matching resolveAndInstantiateESLint.
200
+ const present = option === 'reportUnusedDisableDirectives'
201
+ ? !!value
202
+ : value !== undefined;
203
+ if (present) {
204
+ const label = configuration
205
+ ? `${baseLabel}:${configuration}`
206
+ : baseLabel;
207
+ entries.push(`${label} (${option})`);
208
+ }
209
+ }
210
+ }
211
+ };
212
+ // Project lint targets keep the options the generator does not strip; it already
213
+ // removes `eslintConfig` and `ignorePath`, so exclude ignorePath here.
214
+ const projectCandidates = FLAT_CONFIG_UNSUPPORTED_OPTIONS.filter((option) => option !== 'ignorePath');
215
+ for (const [project, projectConfig] of (0, devkit_1.getProjects)(tree)) {
216
+ for (const [targetName, target] of Object.entries(projectConfig.targets ?? {})) {
217
+ if (target.executor !== ESLINT_LINT_EXECUTOR) {
218
+ continue;
219
+ }
220
+ const optionSets = [
221
+ [null, target.options],
222
+ ...Object.entries(target.configurations ?? {}),
223
+ ];
224
+ collect(`${project}:${targetName}`, optionSets, projectCandidates);
225
+ }
226
+ }
227
+ // targetDefaults inherit into every lint target, including `ignorePath`, which
228
+ // the generator only cleans up at the project level.
229
+ const targetDefaults = (0, devkit_1.readNxJson)(tree)?.targetDefaults ?? {};
230
+ for (const [name, target] of Object.entries(targetDefaults)) {
231
+ if (name !== 'lint' && name !== ESLINT_LINT_EXECUTOR) {
232
+ continue;
233
+ }
234
+ const optionSets = [[null, target.options], ...Object.entries(target.configurations ?? {})];
235
+ collect(`targetDefaults["${name}"]`, optionSets, FLAT_CONFIG_UNSUPPORTED_OPTIONS);
236
+ }
237
+ return entries;
238
+ }
239
+ // Scans the generated flat configs for the FlatCompat shim so the advisory only
240
+ // fires when there is real compat output to assess.
241
+ function generatedConfigsUseFlatCompat(tree) {
242
+ const roots = ['', ...[...(0, devkit_1.getProjects)(tree).values()].map((p) => p.root)];
243
+ for (const root of roots) {
244
+ for (const filename of [
245
+ ...config_file_1.ESLINT_FLAT_CONFIG_FILENAMES,
246
+ ...config_file_1.BASE_ESLINT_CONFIG_FILENAMES,
247
+ ]) {
248
+ const path = root ? `${root}/${filename}` : filename;
249
+ if (!tree.exists(path)) {
250
+ continue;
251
+ }
252
+ if ((tree.read(path, 'utf-8') ?? '').includes('FlatCompat')) {
253
+ return true;
254
+ }
255
+ }
256
+ }
257
+ return false;
258
+ }
259
+ function readEslintrcConfig(tree, path) {
260
+ if (path.endsWith('.yaml') || path.endsWith('.yml')) {
261
+ const content = tree.read(path, 'utf-8');
262
+ if (!content) {
263
+ return null;
264
+ }
265
+ const { load } = require('@zkochan/js-yaml');
266
+ return load(content, { json: true, filename: path });
267
+ }
268
+ try {
269
+ return (0, devkit_1.readJson)(tree, path);
270
+ }
271
+ catch {
272
+ return null;
273
+ }
274
+ }
275
+ function passingBaselineInstruction(userExplicitRules) {
276
+ const ruleList = userExplicitRules.length > 0
277
+ ? `The user explicitly configured these rules before the migration: ${userExplicitRules.join(', ')}.`
278
+ : 'The user did not explicitly configure any rules before the migration.';
279
+ return (`Passing-state requirement: after migrating, run the workspace lint and keep it passing. ${ruleList} ` +
280
+ 'For any rule that now reports errors but is not in that list, disable it in the flat config with a short explanatory comment. ' +
281
+ 'Those errors come from changed preset defaults (the ESLint v9 "eslint:recommended" set and the typescript-eslint v8 recommended sets), not from the user. ' +
282
+ 'Never disable or weaken a rule the user explicitly configured, and never edit source files to satisfy a newly enabled rule.');
283
+ }
@@ -0,0 +1,286 @@
1
+ # ESLint v9 Flat Config Migration Instructions for LLM
2
+
3
+ ## Overview
4
+
5
+ These instructions guide you through finishing the migration of an Nx workspace to ESLint v9.
6
+
7
+ ESLint v9 makes flat config (`eslint.config.{mjs,cjs,js}`) the default config format. The legacy eslintrc format (`.eslintrc.*`) still works at runtime, but only when `ESLINT_USE_FLAT_CONFIG=false` is set, so Nx converts workspaces to flat config instead of relying on that escape hatch.
8
+
9
+ The migration runs in two halves:
10
+
11
+ 1. A deterministic pre-pass (the `@nx/eslint:convert-to-flat-config` generator) that already converted the JSON and YAML eslintrc configs.
12
+ 2. This prompt: finish the parts that need judgment and leave the workspace lint-passing.
13
+
14
+ Work systematically through each section below.
15
+
16
+ <pre_pass_summary note="a deterministic pre-pass already applied these edits; verify the new shape is in place rather than redoing them">
17
+
18
+ The pre-pass handled, mechanically:
19
+
20
+ - Converted the root and per-project JSON/YAML eslintrc files to `eslint.config.mjs`:
21
+ - `eslint:recommended` to `js.configs.recommended`
22
+ - `@nx/*` presets to their flat-config equivalents
23
+ - `env` to `languageOptions.globals`
24
+ - `parser` / `parserOptions` to `languageOptions`
25
+ - `plugins` to the flat `plugins` object
26
+ - `ignorePatterns` and `.eslintignore` to `ignores`
27
+ - stale `.eslintrc`/`.eslintignore` references in `nx.json` and `project.json` inputs
28
+ - Added `@eslint/js` and `@eslint/eslintrc` to `package.json` when the converted config needs them.
29
+
30
+ The pre-pass does NOT:
31
+
32
+ - Convert JavaScript-based eslintrc files (`.eslintrc.js`, `.eslintrc.cjs`). It cannot evaluate them safely.
33
+ - Change the output formatter a lint target uses.
34
+ - Decide whether a generated `FlatCompat` shim should become flat-native config.
35
+ - Make the workspace pass lint after ESLint v9 changed which rules its preset defaults enable.
36
+
37
+ Everything the pre-pass could not finish is forwarded to you in `<advisory_context>`.
38
+
39
+ How to read the wrapper sections above this file:
40
+
41
+ - `<files_changed>` lists files the pre-pass wrote. Verify the new shape is in place; do not re-apply the same edit. It is absent when the pre-pass made no changes (for example a workspace that was already on flat config).
42
+ - `<advisory_context>` lists detections the pre-pass forwarded because it could not safely complete them. Every entry is pending work. Address each one in the relevant section below.
43
+
44
+ </pre_pass_summary>
45
+
46
+ <handoff_guidance>
47
+ In your handoff `summary` (1 to 3 sentences per the system prompt), name the sections you applied and explicitly call out any you skipped because they did not apply (for example "no JavaScript-based configs and no removed formatters in this workspace").
48
+ </handoff_guidance>
49
+
50
+ ## Pre-Migration Checklist
51
+
52
+ 1. **Confirm the ESLint version is v9**:
53
+
54
+ ```bash
55
+ npx eslint --version
56
+ ```
57
+
58
+ 2. **Locate all ESLint config files**:
59
+ - Flat configs: `eslint.config.{mjs,cjs,js}` at the root and in each project.
60
+ - Any remaining eslintrc files: `.eslintrc`, `.eslintrc.json`, `.eslintrc.yaml`, `.eslintrc.yml`, `.eslintrc.js`, `.eslintrc.cjs`.
61
+ - Ignore files: `.eslintignore`.
62
+
63
+ 3. **Identify all lint targets**:
64
+
65
+ ```bash
66
+ nx show projects --with-target lint
67
+ ```
68
+
69
+ Check `project.json` files for the `@nx/eslint:lint` executor or `eslint` run-commands. Workspaces using the inferred plugin (`@nx/eslint/plugin`) get lint targets from the presence of `eslint.config.*`; inspect them with `nx show project <name> --json`.
70
+
71
+ 4. **Identify local ESLint rules or plugins** authored inside the workspace. These use the rule API that changed in v9 (see section 6).
72
+
73
+ ---
74
+
75
+ ## Nx-Specific Notes (read first)
76
+
77
+ - **Flat config is the default in v9**. eslintrc only resolves when `ESLINT_USE_FLAT_CONFIG=false` is set. Nx converts the workspace to flat config so that no environment variable is required.
78
+ - **Shared base config pattern**: many Nx workspaces have a root `eslint.config.mjs` that each project imports, for example `import baseConfig from '../../eslint.config.mjs'`. Convert and verify the base config first, then the per-project configs.
79
+ - **Inferred plugin targets**: `@nx/eslint/plugin` infers the lint target from the presence of `eslint.config.*`. Renaming or moving the config invalidates inference. After config edits, run `nx reset && nx show project <name>` on a sample project to confirm the target is still present.
80
+ - **FlatCompat shim**: when the pre-pass could not translate a third-party `extends` or a complex override natively, it emitted a `FlatCompat` shim (from the `@eslint/eslintrc` package). That config works as-is, but section 3 covers replacing it with flat-native config where low-risk.
81
+
82
+ ---
83
+
84
+ ## 1. Already on flat config? Verify only
85
+
86
+ If the workspace already uses `eslint.config.*` at the root and in every project, with no remaining `.eslintrc.*` files, do NOT restructure it. The only required work is the passing-state check in section 4: a workspace on ESLint v9 can newly fail because v9 and typescript-eslint v8 changed which rules their recommended sets enable, even when the config was already flat.
87
+
88
+ ## 2. Convert JavaScript-based ESLint configs the pre-pass skipped
89
+
90
+ **Search pattern**: `.eslintrc.js` and `.eslintrc.cjs` files (forwarded in `<advisory_context>`).
91
+
92
+ **What changed**: the pre-pass only converts JSON and YAML eslintrc files. JavaScript-based configs run arbitrary code, so they need manual conversion.
93
+
94
+ ```js
95
+ // BEFORE (.eslintrc.js)
96
+ module.exports = {
97
+ extends: ['../../.eslintrc.json'],
98
+ overrides: [
99
+ {
100
+ files: ['*.ts'],
101
+ rules: { '@typescript-eslint/no-explicit-any': 'error' },
102
+ },
103
+ ],
104
+ };
105
+ ```
106
+
107
+ ```js
108
+ // AFTER (eslint.config.mjs)
109
+ import baseConfig from '../../eslint.config.mjs';
110
+
111
+ export default [
112
+ ...baseConfig,
113
+ {
114
+ files: ['**/*.ts'],
115
+ rules: { '@typescript-eslint/no-explicit-any': 'error' },
116
+ },
117
+ ];
118
+ ```
119
+
120
+ **Action items**:
121
+
122
+ - [ ] Convert each JavaScript-based config to `eslint.config.mjs`, mirroring the structure the pre-pass produced for the JSON/YAML configs.
123
+ - [ ] Preserve the existing rules, plugins, parser options, and overrides.
124
+ - [ ] Delete the original `.eslintrc.js` / `.eslintrc.cjs` once the flat config replaces it.
125
+ - [ ] Update any `project.json` / `nx.json` inputs that referenced the old file name.
126
+
127
+ ## 3. Convert FlatCompat shims to flat-native config where low-risk
128
+
129
+ **Search pattern**: `FlatCompat`, `@eslint/eslintrc`, `compat.extends(`, `compat.config(` in the generated `eslint.config.*` files (listed in `<files_changed>`).
130
+
131
+ **What changed**: `FlatCompat` is a runtime shim that adapts eslintrc-style `extends` into flat config. Many plugins now ship native flat presets, which are clearer and avoid the shim.
132
+
133
+ **Decision rule**: convert a `FlatCompat` usage to flat-native config when it is low-risk, otherwise keep the shim.
134
+
135
+ - Low-risk (prefer flat-native): typescript-eslint configs, and plugins that document a flat preset (for example `eslint-plugin-react`, `eslint-plugin-import`).
136
+ - Keep the shim: third-party shared configs that do not document a flat-config entry point.
137
+
138
+ ```js
139
+ // BEFORE (FlatCompat shim, eslint.config.mjs)
140
+ import js from '@eslint/js';
141
+ import { fileURLToPath } from 'url';
142
+ import { dirname } from 'path';
143
+ import { FlatCompat } from '@eslint/eslintrc';
144
+
145
+ const compat = new FlatCompat({
146
+ baseDirectory: dirname(fileURLToPath(import.meta.url)),
147
+ recommendedConfig: js.configs.recommended,
148
+ });
149
+
150
+ export default [...compat.extends('plugin:@typescript-eslint/recommended')];
151
+ ```
152
+
153
+ ```js
154
+ // AFTER (flat-native, eslint.config.mjs)
155
+ import tseslint from 'typescript-eslint';
156
+
157
+ export default [...tseslint.configs.recommended];
158
+ ```
159
+
160
+ **Action items**:
161
+
162
+ - [ ] For each `FlatCompat` usage, decide flat-native vs keep-the-shim using the rule above.
163
+ - [ ] When converting, drop the now-unused `@eslint/eslintrc` import if no shim remains in that file.
164
+ - [ ] Re-run lint after each change to confirm the rule set did not silently shift.
165
+
166
+ ## 4. Restore the passing baseline (required)
167
+
168
+ This is the core requirement of the migration: the workspace must lint cleanly when you are done.
169
+
170
+ ESLint v9 and typescript-eslint v8 changed which rules their recommended sets enable. A rule the user never configured may now report errors. Disable those rules; do not edit source files to satisfy them.
171
+
172
+ The set of rules the user explicitly configured before the migration is in `<advisory_context>` (the entry that starts with "Passing-state requirement").
173
+
174
+ **Procedure**:
175
+
176
+ 1. Run lint across the workspace:
177
+
178
+ ```bash
179
+ nx run-many -t lint
180
+ ```
181
+
182
+ 2. For each rule that now reports errors:
183
+ - If the rule ID is NOT in the user's explicit list, it came from a changed preset default. Disable it in the relevant flat config with a short comment explaining why.
184
+ - If the rule ID IS in the user's explicit list, the user chose it. Leave it as-is and report it in your handoff summary.
185
+
186
+ ```js
187
+ // Disable a rule that a changed preset default newly enabled (eslint.config.mjs).
188
+ export default [
189
+ ...baseConfig,
190
+ {
191
+ files: ['**/*.ts'],
192
+ rules: {
193
+ // Newly enabled by the ESLint v9 recommended set; was not enforced before the upgrade.
194
+ 'no-unused-expressions': 'off',
195
+ },
196
+ },
197
+ ];
198
+ ```
199
+
200
+ **Action items**:
201
+
202
+ - [ ] Run lint and collect every newly reported rule.
203
+ - [ ] Disable preset-originated rules that the user did not configure.
204
+ - [ ] Never disable or weaken a rule the user explicitly configured.
205
+ - [ ] Never edit source files to satisfy a newly enabled rule.
206
+
207
+ <fail_if note="if you cannot reach a passing state without editing source or disabling a user-configured rule, stop and report">
208
+ You cannot make lint pass without either editing source files or disabling a rule the user explicitly configured. Write status: failed and explain which rule and project in your summary. Do not guess.
209
+ </fail_if>
210
+
211
+ ## 5. Fix removed output formatters
212
+
213
+ **Search pattern**: the `format` option on lint targets (forwarded in `<advisory_context>`).
214
+
215
+ **What changed**: ESLint v9 removed several built-in output formatters. The built-ins that remain are `stylish`, `html`, `json`, and `json-with-metadata`. Removed: `compact`, `codeframe`, `unix`, `visualstudio`, `table`, `checkstyle`, `jslint-xml`, `junit`, `tap`.
216
+
217
+ **Fix**: switch the target to a built-in that remains, or install the matching community package and reference it by its package name.
218
+
219
+ ```bash
220
+ # Example: keep junit output by installing the community formatter package.
221
+ npm install --save-dev eslint-formatter-junit
222
+ ```
223
+
224
+ ```jsonc
225
+ // project.json (reference the community formatter by package name)
226
+ "lint": {
227
+ "executor": "@nx/eslint:lint",
228
+ "options": { "format": "eslint-formatter-junit" }
229
+ }
230
+ ```
231
+
232
+ **Action items**:
233
+
234
+ - [ ] For each flagged target, switch to a remaining built-in formatter or a community package.
235
+ - [ ] When using a community package, add it to `devDependencies`.
236
+
237
+ ## 6. Other ESLint v9 runtime breaking changes
238
+
239
+ **Search pattern**: lint executor options, run-commands invoking `eslint`, and local rule/plugin source.
240
+
241
+ - **Removed CLI flags and executor options**: `--rulesdir`, `--ext`, and `--resolve-plugins-relative-to` were removed. The matching `@nx/eslint:lint` options (`rulesdir`, `resolvePluginsRelativeTo`, `ignorePath`) are not supported for flat config. Move file targeting into the config via the `files` and `ignores` keys.
242
+ - **No eslintrc auto-merge**: flat config does not merge `.eslintrc.*` files found up the tree. Every setting must live in `eslint.config.*`.
243
+ - **Local rule API moved to `SourceCode`** (only relevant if the workspace authors its own rules):
244
+ - `context.getScope()` to `sourceCode.getScope(node)`
245
+ - `context.getAncestors()` to `sourceCode.getAncestors(node)`
246
+ - `context.getDeclaredVariables()` to `sourceCode.getDeclaredVariables(node)`
247
+ - `context.markVariableAsUsed(name)` to `sourceCode.markVariableAsUsed(name, node)`
248
+ - `context.getSource()` to `sourceCode.getText()`
249
+ - `context.parserServices` to `sourceCode.parserServices`
250
+ - **Stricter rule schema**: a custom rule that accepts options must declare `meta.schema` in v9.
251
+
252
+ **Action items**:
253
+
254
+ - [ ] Remove unsupported CLI flags and executor options; move targeting into `files` / `ignores`.
255
+ - [ ] Update local rules to the `SourceCode` API and add `meta.schema` where required.
256
+
257
+ ---
258
+
259
+ ## Post-Migration Verification
260
+
261
+ 1. Clear the inference cache so renamed configs are re-detected:
262
+
263
+ ```bash
264
+ nx reset
265
+ ```
266
+
267
+ 2. Confirm lint passes across the workspace:
268
+
269
+ ```bash
270
+ nx run-many -t lint
271
+ ```
272
+
273
+ 3. Spot-check that a converted project resolves its config:
274
+
275
+ ```bash
276
+ npx eslint --print-config <a-file-in-the-project>
277
+ ```
278
+
279
+ 4. Confirm no `.eslintrc.*` files remain unless one was intentionally kept.
280
+
281
+ ## References
282
+
283
+ - ESLint configuration files (flat config): https://eslint.org/docs/latest/use/configure/configuration-files
284
+ - Migrate to ESLint v9.0.0: https://eslint.org/docs/latest/use/migrate-to-9.0.0
285
+ - typescript-eslint configs: https://typescript-eslint.io/users/configs
286
+ - Nx ESLint plugin: https://nx.dev/nx-api/eslint
@@ -12,7 +12,6 @@ const file_hasher_1 = require("nx/src/hasher/file-hasher");
12
12
  const cache_directory_1 = require("nx/src/utils/cache-directory");
13
13
  const globs_1 = require("nx/src/utils/globs");
14
14
  const workspace_context_1 = require("nx/src/utils/workspace-context");
15
- const semver_1 = require("semver");
16
15
  const config_file_1 = require("../utils/config-file");
17
16
  const resolve_eslint_class_1 = require("../utils/resolve-eslint-class");
18
17
  const DEFAULT_EXTENSIONS = [
@@ -34,7 +33,6 @@ const ESLINT_CONFIG_GLOB_V2 = (0, globs_1.combineGlobPatterns)([
34
33
  ]);
35
34
  const internalCreateNodesV2 = async (ESLint, configFilePath, options, context, projectRootsByEslintRoots, lintableFilesPerProjectRoot, tsconfigChainsByProjectRoot, projectsCache, hashByRoot, pmc) => {
36
35
  const configDir = (0, posix_1.dirname)(configFilePath);
37
- const eslintVersion = ESLint.version;
38
36
  let sharedEslint;
39
37
  const getEslint = (projectRoot) => {
40
38
  if ((0, node_fs_1.existsSync)((0, posix_1.join)(context.workspaceRoot, projectRoot, '.eslintignore'))) {
@@ -75,7 +73,7 @@ const internalCreateNodesV2 = async (ESLint, configFilePath, options, context, p
75
73
  projectsCache.set(hash, {});
76
74
  return null;
77
75
  }
78
- const project = getProjectUsingESLintConfig(configFilePath, projectRoot, eslintVersion, options, context, pmc, tsconfigChainsByProjectRoot.get(projectRoot) ?? []);
76
+ const project = getProjectUsingESLintConfig(configFilePath, projectRoot, options, context, pmc, tsconfigChainsByProjectRoot.get(projectRoot) ?? []);
79
77
  if (project) {
80
78
  const entry = { [projectRoot]: project };
81
79
  // Store project into the cache
@@ -250,7 +248,7 @@ function getRootForDirectory(directory, roots) {
250
248
  }
251
249
  return roots.has(currentPath) ? currentPath : null;
252
250
  }
253
- function getProjectUsingESLintConfig(configFilePath, projectRoot, eslintVersion, options, context, pmc, tsconfigChainOutsideProjectRoot) {
251
+ function getProjectUsingESLintConfig(configFilePath, projectRoot, options, context, pmc, tsconfigChainOutsideProjectRoot) {
254
252
  const rootEslintConfig = [
255
253
  config_file_1.baseEsLintConfigFile,
256
254
  ...config_file_1.BASE_ESLINT_CONFIG_FILENAMES,
@@ -275,10 +273,10 @@ function getProjectUsingESLintConfig(configFilePath, projectRoot, eslintVersion,
275
273
  eslintConfigs.unshift(rootEslintConfig);
276
274
  }
277
275
  return {
278
- targets: buildEslintTargets(eslintConfigs, eslintVersion, projectRoot, context.workspaceRoot, options, pmc, standaloneSrcPath, tsconfigChainOutsideProjectRoot),
276
+ targets: buildEslintTargets(eslintConfigs, projectRoot, context.workspaceRoot, options, pmc, standaloneSrcPath, tsconfigChainOutsideProjectRoot),
279
277
  };
280
278
  }
281
- function buildEslintTargets(eslintConfigs, eslintVersion, projectRoot, workspaceRoot, options, pmc, standaloneSrcPath, tsconfigChainOutsideProjectRoot = []) {
279
+ function buildEslintTargets(eslintConfigs, projectRoot, workspaceRoot, options, pmc, standaloneSrcPath, tsconfigChainOutsideProjectRoot = []) {
282
280
  const isRootProject = projectRoot === '.';
283
281
  const targets = {};
284
282
  const targetConfig = {
@@ -315,13 +313,12 @@ function buildEslintTargets(eslintConfigs, eslintVersion, projectRoot, workspace
315
313
  },
316
314
  },
317
315
  };
318
- // Always set the environment variable to ensure that the ESLint CLI can run on eslint v8 and v9
316
+ // Supported ESLint versions (v9+) default to flat config, so only set the env
317
+ // var when the workspace still uses eslintrc, to force the legacy loader.
319
318
  const useFlatConfig = eslintConfigs.some((config) => (0, config_file_1.isFlatConfig)(config));
320
- // Flat config is default for 9.0.0+
321
- const defaultSetting = (0, semver_1.gte)(eslintVersion, '9.0.0');
322
- if (useFlatConfig !== defaultSetting) {
319
+ if (!useFlatConfig) {
323
320
  targetConfig.options.env = {
324
- ESLINT_USE_FLAT_CONFIG: useFlatConfig ? 'true' : 'false',
321
+ ESLINT_USE_FLAT_CONFIG: 'false',
325
322
  };
326
323
  }
327
324
  targets[options.targetName] = targetConfig;
@@ -2,11 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.assertSupportedEslintVersion = assertSupportedEslintVersion;
4
4
  const internal_1 = require("@nx/devkit/internal");
5
- const deprecation_1 = require("./deprecation");
6
5
  const versions_1 = require("./versions");
7
6
  function assertSupportedEslintVersion(tree) {
8
7
  (0, internal_1.assertSupportedPackageVersion)(tree, 'eslint', versions_1.minSupportedEslintVersion);
9
- if ((0, versions_1.getInstalledEslintMajorVersion)(tree) === 8) {
10
- (0, deprecation_1.warnEslintV8Deprecation)();
11
- }
12
8
  }
@@ -8,14 +8,7 @@ const fs_1 = require("fs");
8
8
  const path_1 = require("path");
9
9
  const flat_config_1 = require("./flat-config");
10
10
  exports.ESLINT_FLAT_CONFIG_FILENAMES = flat_config_1.eslintFlatConfigFilenames;
11
- exports.ESLINT_OLD_CONFIG_FILENAMES = [
12
- '.eslintrc',
13
- '.eslintrc.js',
14
- '.eslintrc.cjs',
15
- '.eslintrc.yaml',
16
- '.eslintrc.yml',
17
- '.eslintrc.json',
18
- ];
11
+ exports.ESLINT_OLD_CONFIG_FILENAMES = flat_config_1.eslintrcFilenames;
19
12
  exports.ESLINT_CONFIG_FILENAMES = [
20
13
  ...exports.ESLINT_OLD_CONFIG_FILENAMES,
21
14
  ...exports.ESLINT_FLAT_CONFIG_FILENAMES,
@@ -1,5 +1,3 @@
1
1
  export declare const ESLINT_EXECUTOR_DEPRECATION_MESSAGE = "The `@nx/eslint:lint` executor is deprecated and will be removed in Nx v24. Run `nx g @nx/eslint:convert-to-inferred` to migrate to the `@nx/eslint/plugin` inferred targets. See https://nx.dev/docs/guides/tasks--caching/convert-to-inferred for details.";
2
2
  export declare function warnEslintExecutorDeprecation(): void;
3
3
  export declare function warnEslintExecutorGenerating(): void;
4
- export declare const ESLINT_V8_DEPRECATION_MESSAGE = "Support for ESLint v8 is deprecated and will be removed in Nx v24. Please upgrade to ESLint v9.";
5
- export declare function warnEslintV8Deprecation(): void;