@nx/eslint 23.1.0-beta.5 → 23.1.0-beta.6

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.
@@ -0,0 +1,20 @@
1
+ import { type Tree } from '@nx/devkit';
2
+ /**
3
+ * Reconciles converted flat configs with angular-eslint v22's breaking changes so
4
+ * they load again. The converter carries the removed `plugin:@angular-eslint/*`
5
+ * configs as FlatCompat shims in two shapes: a bare `...compat.extends(...)` for
6
+ * top-level extends, and a `...compat.config({ extends }).map(...)` for per-override
7
+ * extends. This handles both: shared configs become their flat-native
8
+ * `angular.configs.*` counterparts, and `process-inline-templates` becomes a
9
+ * `processor` block. The per-override shape drops that block when `flat/angular`
10
+ * already applies the processor; the top-level shape keeps it. It also drops the
11
+ * removed `no-conflicting-lifecycle` rule, injects the `angular-eslint` import
12
+ * (and dependency) when it introduces `angular.*` references, and removes the
13
+ * FlatCompat scaffolding left unused afterwards.
14
+ *
15
+ * Gated on angular-eslint v22+: the shims resolve on v18-v21, so rewriting them
16
+ * there would be churn, and only the v22 flat exports are known here. Does not
17
+ * format; the caller owns formatting.
18
+ */
19
+ export declare function migrateAngularEslintV22FlatConfig(tree: Tree): Promise<void>;
20
+ export declare function resolveAngularEslintVersion(tree: Tree): string;
@@ -0,0 +1,410 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.migrateAngularEslintV22FlatConfig = migrateAngularEslintV22FlatConfig;
4
+ exports.resolveAngularEslintVersion = resolveAngularEslintVersion;
5
+ const tslib_1 = require("tslib");
6
+ const devkit_1 = require("@nx/devkit");
7
+ const semver_1 = require("semver");
8
+ const ts = tslib_1.__importStar(require("typescript"));
9
+ const config_file_1 = require("../../utils/config-file");
10
+ const ast_utils_1 = require("../utils/flat-config/ast-utils");
11
+ // angular-eslint v22 dropped the legacy eslintrc config format, so every
12
+ // `plugin:@angular-eslint/*` shared config stops resolving. The converter emits
13
+ // them as `compat.extends(...)` shims (it maps only `plugin:@nx/*` natively), so
14
+ // those shims fail to load. Each removed eslintrc config has a flat-native
15
+ // counterpart on the umbrella `angular-eslint` package; remap the shims to it.
16
+ const ANGULAR_ESLINT_CONFIG_MAP = {
17
+ 'plugin:@angular-eslint/recommended': 'tsRecommended',
18
+ 'plugin:@angular-eslint/all': 'tsAll',
19
+ 'plugin:@angular-eslint/template/recommended': 'templateRecommended',
20
+ 'plugin:@angular-eslint/template/accessibility': 'templateAccessibility',
21
+ 'plugin:@angular-eslint/template/all': 'templateAll',
22
+ };
23
+ // Not a shared config but a processor: v22 exposes it as `processInlineTemplates`,
24
+ // applied via a `{ files, processor }` block rather than an extends spread.
25
+ const PROCESS_INLINE_TEMPLATES = 'plugin:@angular-eslint/template/process-inline-templates';
26
+ // angular-eslint v22 removed this rule; a config that still lists it throws on
27
+ // load: "Could not find no-conflicting-lifecycle in plugin @angular-eslint".
28
+ const REMOVED_RULE = '@angular-eslint/no-conflicting-lifecycle';
29
+ const FLAT_CONFIG_FILENAMES = new Set([
30
+ ...config_file_1.ESLINT_FLAT_CONFIG_FILENAMES,
31
+ ...config_file_1.BASE_ESLINT_CONFIG_FILENAMES,
32
+ ]);
33
+ /**
34
+ * Reconciles converted flat configs with angular-eslint v22's breaking changes so
35
+ * they load again. The converter carries the removed `plugin:@angular-eslint/*`
36
+ * configs as FlatCompat shims in two shapes: a bare `...compat.extends(...)` for
37
+ * top-level extends, and a `...compat.config({ extends }).map(...)` for per-override
38
+ * extends. This handles both: shared configs become their flat-native
39
+ * `angular.configs.*` counterparts, and `process-inline-templates` becomes a
40
+ * `processor` block. The per-override shape drops that block when `flat/angular`
41
+ * already applies the processor; the top-level shape keeps it. It also drops the
42
+ * removed `no-conflicting-lifecycle` rule, injects the `angular-eslint` import
43
+ * (and dependency) when it introduces `angular.*` references, and removes the
44
+ * FlatCompat scaffolding left unused afterwards.
45
+ *
46
+ * Gated on angular-eslint v22+: the shims resolve on v18-v21, so rewriting them
47
+ * there would be churn, and only the v22 flat exports are known here. Does not
48
+ * format; the caller owns formatting.
49
+ */
50
+ async function migrateAngularEslintV22FlatConfig(tree) {
51
+ if (!isAngularEslintV22OrLater(tree)) {
52
+ return;
53
+ }
54
+ let needsAngularDependency = false;
55
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '.', (path) => {
56
+ const fileName = path.split('/').pop();
57
+ if (!fileName || !FLAT_CONFIG_FILENAMES.has(fileName)) {
58
+ return;
59
+ }
60
+ const content = tree.read(path, 'utf-8');
61
+ if (!content || !referencesRemovedAngularEslint(content)) {
62
+ return;
63
+ }
64
+ const { updated, introducedAngularRef } = rewriteContent(content);
65
+ if (updated !== content) {
66
+ tree.write(path, updated);
67
+ needsAngularDependency ||= introducedAngularRef;
68
+ }
69
+ });
70
+ if (needsAngularDependency) {
71
+ (0, devkit_1.addDependenciesToPackageJson)(tree, {}, { 'angular-eslint': resolveAngularEslintVersion(tree) }, 'package.json', true);
72
+ }
73
+ }
74
+ // The umbrella `angular-eslint` and the scoped `@angular-eslint/*` packages
75
+ // release in lockstep, so any one reflects the declared version. `nx migrate`
76
+ // writes the version bump before migrations run, and the standalone generator
77
+ // reads the currently installed version; either way package.json is the source.
78
+ function readAngularEslintVersion(tree) {
79
+ return ((0, devkit_1.getDependencyVersionFromPackageJson)(tree, '@angular-eslint/eslint-plugin') ??
80
+ (0, devkit_1.getDependencyVersionFromPackageJson)(tree, 'angular-eslint') ??
81
+ (0, devkit_1.getDependencyVersionFromPackageJson)(tree, '@angular-eslint/template-parser'));
82
+ }
83
+ // Pin the umbrella to the major already installed, falling back to the latest
84
+ // major nx generates when none is present. @nx/eslint can't read the canonical
85
+ // pin from @nx/angular without inverting the package dependency (@nx/angular
86
+ // depends on @nx/eslint), so the fallback is hardcoded; keep it in sync with
87
+ // `angularEslintVersion` in packages/angular/src/utils/versions.ts.
88
+ function resolveAngularEslintVersion(tree) {
89
+ const version = readAngularEslintVersion(tree);
90
+ const major = version ? (0, semver_1.coerce)(version)?.major : undefined;
91
+ return major != null ? `^${major}.0.0` : '^22.0.0';
92
+ }
93
+ function isAngularEslintV22OrLater(tree) {
94
+ const version = readAngularEslintVersion(tree);
95
+ const major = version ? (0, semver_1.coerce)(version)?.major : undefined;
96
+ return major != null && major >= 22;
97
+ }
98
+ // Cheap pre-filter so untouched configs are neither parsed nor rewritten.
99
+ function referencesRemovedAngularEslint(content) {
100
+ return (content.includes(REMOVED_RULE) ||
101
+ content.includes(PROCESS_INLINE_TEMPLATES) ||
102
+ Object.keys(ANGULAR_ESLINT_CONFIG_MAP).some((config) => content.includes(config)));
103
+ }
104
+ function rewriteContent(content) {
105
+ // `flat/angular` (from `plugin:@nx/angular`) already applies the inline-template
106
+ // processor, so a process-inline-templates shim alongside it is redundant.
107
+ const flatAngularPresent = /flat\/angular['"]/.test(content);
108
+ // Rewrite the compat shims first, then drop the removed rule on the result: a
109
+ // rule can live inside a shim's `.map` body that pass 1 collapses, so doing
110
+ // both in one pass would collect overlapping edits.
111
+ const { updated: rewritten, introducedAngularRef } = rewriteAngularShims(content, flatAngularPresent);
112
+ const updated = removeRule(rewritten, REMOVED_RULE);
113
+ if (updated === content) {
114
+ return { updated: content, introducedAngularRef: false };
115
+ }
116
+ let cleaned = removeOrphanedFlatCompat(updated);
117
+ if (introducedAngularRef) {
118
+ cleaned = (0, ast_utils_1.addImportToFlatConfig)(cleaned, 'angular', 'angular-eslint');
119
+ }
120
+ return { updated: cleaned, introducedAngularRef };
121
+ }
122
+ // Rewrites both shapes the converter emits for angular-eslint extends: the bare
123
+ // `...compat.extends(...)` spread (top-level extends) and the
124
+ // `...compat.config({ extends: [...] }).map(...)` spread (per-override extends).
125
+ function rewriteAngularShims(content, flatAngularPresent) {
126
+ const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
127
+ const changes = [];
128
+ let introducedAngularRef = false;
129
+ const replaceSpread = (node, replacement) => {
130
+ const start = node.getStart(source);
131
+ changes.push({ type: devkit_1.ChangeType.Delete, start, length: node.end - start });
132
+ changes.push({ type: devkit_1.ChangeType.Insert, index: start, text: replacement });
133
+ };
134
+ const visit = (node) => {
135
+ if (ts.isSpreadElement(node) && ts.isCallExpression(node.expression)) {
136
+ // Shape 1: `...compat.extends('plugin:@angular-eslint/...', ...)`
137
+ if (node.expression.expression.getText(source) === 'compat.extends') {
138
+ const replacement = buildExtendsReplacement(node.expression.arguments);
139
+ if (replacement !== null) {
140
+ replaceSpread(node, replacement);
141
+ introducedAngularRef = true;
142
+ }
143
+ }
144
+ else {
145
+ // Shape 2: `...compat.config({ extends: [...] }).map(config => (...))`
146
+ const replacement = buildConfigOverrideReplacement(node, source, flatAngularPresent);
147
+ if (replacement !== null) {
148
+ replaceSpread(node, replacement.text);
149
+ introducedAngularRef ||= replacement.introducedAngularRef;
150
+ }
151
+ }
152
+ }
153
+ ts.forEachChild(node, visit);
154
+ };
155
+ visit(source);
156
+ return {
157
+ updated: changes.length ? (0, devkit_1.applyChangesToString)(content, changes) : content,
158
+ introducedAngularRef,
159
+ };
160
+ }
161
+ // Removes every `<rule>: ...` property assignment (with its trailing comma and
162
+ // any trailing comment) so the config loads once the rule no longer exists.
163
+ function removeRule(content, rule) {
164
+ const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
165
+ const changes = [];
166
+ const visit = (node) => {
167
+ if (ts.isPropertyAssignment(node) &&
168
+ (ts.isStringLiteral(node.name) ||
169
+ ts.isNoSubstitutionTemplateLiteral(node.name)) &&
170
+ node.name.text === rule) {
171
+ // `getStart` (not `getFullStart`) so leading trivia is left intact; that
172
+ // trivia includes any trailing comment on the previous property.
173
+ const start = node.getStart(source);
174
+ changes.push({
175
+ type: devkit_1.ChangeType.Delete,
176
+ start,
177
+ length: consumeTrailingComma(content, node.end) - start,
178
+ });
179
+ }
180
+ ts.forEachChild(node, visit);
181
+ };
182
+ visit(source);
183
+ return changes.length ? (0, devkit_1.applyChangesToString)(content, changes) : content;
184
+ }
185
+ // Sorts one `extends` entry into the flat-native config it maps to, the inline-
186
+ // template processor, or an unrelated config left in a residual compat shim.
187
+ // Shared by both shim builders so the classification lives in one place.
188
+ function classifyExtend(value) {
189
+ const remapped = ANGULAR_ESLINT_CONFIG_MAP[value];
190
+ if (remapped) {
191
+ return { kind: 'remapped', config: remapped };
192
+ }
193
+ if (value === PROCESS_INLINE_TEMPLATES) {
194
+ return { kind: 'processor' };
195
+ }
196
+ return { kind: 'other', value };
197
+ }
198
+ // Rewrites the per-override shim shape
199
+ // `...compat.config({ extends: [E] }).map(config => ({ ...config, files, rules }))`.
200
+ // Shared configs in `extends` become `angular.configs.*` scoped to the override's
201
+ // files; process-inline-templates becomes a processor block (dropped when
202
+ // `flat/angular` already applies it). When `extends` empties, the
203
+ // `compat.config(...).map(...)` wrapper collapses to the plain `{ files, rules }`
204
+ // block it was scoping. Returns null when the block has no removed angular-eslint
205
+ // config, or has a shape beyond a plain `{ extends }` object (left untouched
206
+ // rather than risk corrupting it).
207
+ function buildConfigOverrideReplacement(node, source, flatAngularPresent) {
208
+ const mapCall = node.expression;
209
+ // Structural match (not `getText`) so it survives prettier wrapping the
210
+ // `compat.config(...).map(...)` chain across lines in an already-formatted config.
211
+ if (!ts.isCallExpression(mapCall) ||
212
+ mapCall.arguments.length !== 1 ||
213
+ !ts.isArrowFunction(mapCall.arguments[0]) ||
214
+ !ts.isPropertyAccessExpression(mapCall.expression) ||
215
+ mapCall.expression.name.text !== 'map' ||
216
+ !ts.isCallExpression(mapCall.expression.expression)) {
217
+ return null;
218
+ }
219
+ const configCall = mapCall.expression.expression;
220
+ if (!ts.isPropertyAccessExpression(configCall.expression) ||
221
+ !ts.isIdentifier(configCall.expression.expression) ||
222
+ configCall.expression.expression.text !== 'compat' ||
223
+ configCall.expression.name.text !== 'config') {
224
+ return null;
225
+ }
226
+ // Only handle a plain `{ extends: [...string literals] }` object; a block that
227
+ // also carries plugins/env/etc. is left alone rather than partially rewritten.
228
+ const configArg = configCall.arguments[0];
229
+ if (!configArg || !ts.isObjectLiteralExpression(configArg)) {
230
+ return null;
231
+ }
232
+ const extendsProp = configArg.properties.find((prop) => ts.isPropertyAssignment(prop) &&
233
+ ts.isIdentifier(prop.name) &&
234
+ prop.name.text === 'extends');
235
+ if (!extendsProp ||
236
+ configArg.properties.length !== 1 ||
237
+ !ts.isArrayLiteralExpression(extendsProp.initializer) ||
238
+ !extendsProp.initializer.elements.every((el) => ts.isStringLiteral(el))) {
239
+ return null;
240
+ }
241
+ const remappedConfigs = [];
242
+ const otherConfigs = [];
243
+ let hasProcessor = false;
244
+ for (const element of extendsProp.initializer.elements) {
245
+ const classified = classifyExtend(element.text);
246
+ if (classified.kind === 'remapped') {
247
+ remappedConfigs.push(classified.config);
248
+ }
249
+ else if (classified.kind === 'processor') {
250
+ hasProcessor = true;
251
+ }
252
+ else {
253
+ otherConfigs.push(classified.value);
254
+ }
255
+ }
256
+ if (!remappedConfigs.length && !hasProcessor) {
257
+ return null;
258
+ }
259
+ const arrow = mapCall.arguments[0];
260
+ // The collapse branch strips the arrow's `...param` spreads by name, so the
261
+ // param must be a plain identifier; bail otherwise rather than mangle the body.
262
+ const mapParam = arrow.parameters[0]?.name;
263
+ if (!mapParam || !ts.isIdentifier(mapParam)) {
264
+ return null;
265
+ }
266
+ const paramName = mapParam.text;
267
+ const body = arrow.body;
268
+ if (!ts.isParenthesizedExpression(body) ||
269
+ !ts.isObjectLiteralExpression(body.expression)) {
270
+ return null;
271
+ }
272
+ const filesProp = body.expression.properties.find((prop) => ts.isPropertyAssignment(prop) &&
273
+ ts.isIdentifier(prop.name) &&
274
+ prop.name.text === 'files');
275
+ if (!filesProp) {
276
+ return null;
277
+ }
278
+ const filesText = filesProp.initializer.getText(source);
279
+ const elements = [];
280
+ let introducedAngularRef = false;
281
+ for (const config of remappedConfigs) {
282
+ // Scope the shared config to the override's files, as the `.map` did.
283
+ elements.push(`...angular.configs.${config}.map((c) => ({ ...c, files: ${filesText} }))`);
284
+ introducedAngularRef = true;
285
+ }
286
+ if (hasProcessor && !flatAngularPresent) {
287
+ elements.push(`{ files: ${filesText}, processor: angular.processInlineTemplates }`);
288
+ introducedAngularRef = true;
289
+ }
290
+ if (otherConfigs.length) {
291
+ // Unrelated configs stay in a compat.config, keeping the override's
292
+ // files/rules through the original callback.
293
+ elements.push(`...compat.config({ extends: [${otherConfigs
294
+ .map((config) => `'${config}'`)
295
+ .join(', ')}] }).map(${arrow.getText(source)})`);
296
+ }
297
+ else {
298
+ // Nothing left to extend: the `.map` only scoped the extended configs, so
299
+ // collapse it to the plain object it was building, dropping the arrow's now-
300
+ // orphaned `...param` / `...param.rules` spreads once the wrapper that bound
301
+ // the param is gone. The negative lookahead keeps `...param` from matching a
302
+ // longer identifier that merely starts with it.
303
+ const orphanedSpreads = new RegExp(`\\.\\.\\.${escapeRegExp(paramName)}(?![\\w$])(?:\\.rules)?\\s*,?\\s*`, 'g');
304
+ elements.push(body.expression.getText(source).replace(orphanedSpreads, ''));
305
+ }
306
+ return { text: elements.join(',\n'), introducedAngularRef };
307
+ }
308
+ // Rewrites a `compat.extends(...)` shim's arguments in place: each removed
309
+ // angular-eslint config becomes an `angular.configs.*` spread, process-inline-
310
+ // templates becomes a processor block, and any unrelated configs stay in a
311
+ // residual `compat.extends`. Argument order is preserved (flat config is
312
+ // last-wins, so reordering would change rule precedence). Returns the joined
313
+ // replacement array elements, or null when no argument is a config nx removed
314
+ // (leave the shim untouched).
315
+ function buildExtendsReplacement(args) {
316
+ // The converter only ever emits string-literal arguments; bail on anything
317
+ // else rather than risk dropping an argument shape we don't understand.
318
+ if (!args.every((arg) => ts.isStringLiteral(arg))) {
319
+ return null;
320
+ }
321
+ const elements = [];
322
+ let pendingOtherConfigs = [];
323
+ let replacedAny = false;
324
+ // Emit the run of unrelated configs seen so far as one `compat.extends(...)`
325
+ // at their original position, so they keep their precedence relative to the
326
+ // remapped configs around them.
327
+ const flushOtherConfigs = () => {
328
+ if (pendingOtherConfigs.length) {
329
+ elements.push(`...compat.extends(${pendingOtherConfigs
330
+ .map((config) => `'${config}'`)
331
+ .join(', ')})`);
332
+ pendingOtherConfigs = [];
333
+ }
334
+ };
335
+ for (const arg of args) {
336
+ const classified = classifyExtend(arg.text);
337
+ if (classified.kind === 'remapped') {
338
+ flushOtherConfigs();
339
+ elements.push(`...angular.configs.${classified.config}`);
340
+ replacedAny = true;
341
+ }
342
+ else if (classified.kind === 'processor') {
343
+ flushOtherConfigs();
344
+ elements.push(`{ files: ['**/*.ts'], processor: angular.processInlineTemplates }`);
345
+ replacedAny = true;
346
+ }
347
+ else {
348
+ pendingOtherConfigs.push(classified.value);
349
+ }
350
+ }
351
+ flushOtherConfigs();
352
+ return replacedAny ? elements.join(',\n') : null;
353
+ }
354
+ // Extends past whitespace, comments, the trailing comma after `end`, and a
355
+ // same-line comment after that comma so the enclosing object/array stays valid
356
+ // after a deletion. Comments before the comma are skipped too: a comment between
357
+ // the value and its comma would otherwise leave a dangling leading comma (a
358
+ // syntax error); a comment after the comma belonged to the removed entry.
359
+ function consumeTrailingComma(content, end) {
360
+ const trailing = content
361
+ .slice(end)
362
+ .match(/^(?:\s|\/\/[^\n]*\n?|\/\*[\s\S]*?\*\/)*,[ \t]*(?:\/\/[^\n]*)?/);
363
+ return trailing ? end + trailing[0].length : end;
364
+ }
365
+ // After the shims are rewritten, the `const compat = new FlatCompat(...)`
366
+ // declaration, its `@eslint/eslintrc` import, and the `@eslint/js` import that
367
+ // fed its `recommendedConfig` may be left unused. Drop each once nothing else
368
+ // references it, so the config carries no dead FlatCompat scaffolding.
369
+ function removeOrphanedFlatCompat(content) {
370
+ if (!content.includes('new FlatCompat')) {
371
+ return content;
372
+ }
373
+ const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
374
+ const compatDeclaration = source.statements.find((node) => ts.isVariableStatement(node) &&
375
+ node.declarationList.declarations.some((decl) => ts.isIdentifier(decl.name) && decl.name.text === 'compat'));
376
+ // The declaration binding is itself one `compat` identifier; anything beyond
377
+ // it means the config still uses `compat`, so leave the scaffolding in place.
378
+ if (!compatDeclaration || countIdentifier(content, 'compat') > 1) {
379
+ return content;
380
+ }
381
+ let updated = content.slice(0, compatDeclaration.getFullStart()) +
382
+ content.slice(compatDeclaration.end);
383
+ updated = (0, ast_utils_1.removeImportFromFlatConfig)(updated, 'FlatCompat', '@eslint/eslintrc');
384
+ // FlatCompat's `recommendedConfig: js.configs.recommended` is usually the only
385
+ // use of `@eslint/js`; once the declaration is gone and nothing else
386
+ // references `js`, drop that import too.
387
+ if (countIdentifier(updated, 'js') <= 1) {
388
+ updated = (0, ast_utils_1.removeImportFromFlatConfig)(updated, 'js', '@eslint/js');
389
+ }
390
+ return updated;
391
+ }
392
+ // Counts identifiers with the given name (declarations, bindings, and
393
+ // references alike) so callers can tell an orphaned binding from a live one.
394
+ function countIdentifier(content, name) {
395
+ const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
396
+ let count = 0;
397
+ const visit = (node) => {
398
+ if (ts.isIdentifier(node) && node.text === name) {
399
+ count++;
400
+ }
401
+ ts.forEachChild(node, visit);
402
+ };
403
+ visit(source);
404
+ return count;
405
+ }
406
+ // Escapes regex metacharacters so a dynamic identifier (an arrow parameter name)
407
+ // can be matched literally. Identifiers can legitimately contain `$`.
408
+ function escapeRegExp(value) {
409
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
410
+ }
@@ -8,8 +8,8 @@ const path_1 = require("path");
8
8
  const assert_supported_eslint_version_1 = require("../../utils/assert-supported-eslint-version");
9
9
  const versions_1 = require("../../utils/versions");
10
10
  const config_file_1 = require("../../utils/config-file");
11
- const semver_1 = require("semver");
12
11
  const json_converter_1 = require("./converters/json-converter");
12
+ const angular_eslint_1 = require("./angular-eslint");
13
13
  async function convertToFlatConfigGenerator(tree, options) {
14
14
  (0, assert_supported_eslint_version_1.assertSupportedEslintVersion)(tree);
15
15
  // Already on flat config at the root? There is nothing to convert.
@@ -43,7 +43,10 @@ async function convertToFlatConfigGenerator(tree, options) {
43
43
  // replace references in nx.json and project.json files
44
44
  updateNxJsonConfig(tree, options.eslintConfigFormat);
45
45
  updateProjectConfigsInputs(tree, options.eslintConfigFormat);
46
- // install missing packages
46
+ // The converter carries angular-eslint's removed eslintrc configs over as
47
+ // FlatCompat shims; on v22 those no longer resolve, so reconcile them to the
48
+ // flat-native config before formatting (no-op below v22).
49
+ await (0, angular_eslint_1.migrateAngularEslintV22FlatConfig)(tree);
47
50
  if (!options.skipFormat) {
48
51
  await (0, devkit_1.formatFiles)(tree);
49
52
  }
@@ -289,7 +292,7 @@ function processConvertedConfig(tree, root, source, target, { content, addESLint
289
292
  // The flat/angular presets import the umbrella `angular-eslint` package; add
290
293
  // it when the converted config references them so the result resolves.
291
294
  if (content.includes('flat/angular')) {
292
- devDependencies['angular-eslint'] = resolveAngularEslintVersion(tree);
295
+ devDependencies['angular-eslint'] = (0, angular_eslint_1.resolveAngularEslintVersion)(tree);
293
296
  }
294
297
  // Direct invocation is an opt-in upgrade, so by default existing pins are
295
298
  // overwritten to land the workspace on the latest flat-config-ready stack.
@@ -297,15 +300,3 @@ function processConvertedConfig(tree, root, source, target, { content, addESLint
297
300
  // `packageJsonUpdates` and only newly added packages are installed here.
298
301
  (0, devkit_1.addDependenciesToPackageJson)(tree, {}, devDependencies, 'package.json', keepExistingVersions);
299
302
  }
300
- // The umbrella `angular-eslint` and the scoped `@angular-eslint/*` packages
301
- // release in lockstep, so pin the umbrella to the major already installed,
302
- // falling back to the latest major nx generates when none is present. @nx/eslint
303
- // can't read the canonical pin from @nx/angular without inverting the package
304
- // dependency (@nx/angular depends on @nx/eslint), so the fallback is hardcoded;
305
- // keep it in sync with `angularEslintVersion` in packages/angular/src/utils/versions.ts.
306
- function resolveAngularEslintVersion(tree) {
307
- const installed = (0, devkit_1.getDependencyVersionFromPackageJson)(tree, '@angular-eslint/eslint-plugin') ??
308
- (0, devkit_1.getDependencyVersionFromPackageJson)(tree, '@angular-eslint/template-parser');
309
- const installedMajor = installed ? (0, semver_1.coerce)(installed)?.major : undefined;
310
- return installedMajor != null ? `^${installedMajor}.0.0` : '^22.0.0';
311
- }
@@ -576,16 +576,23 @@ function removeImportFromFlatConfigESM(source, content, variable, imp) {
576
576
  function removeImportFromFlatConfigCJS(source, content, variable, imp) {
577
577
  const changes = [];
578
578
  ts.forEachChild(source, (node) => {
579
- // we can only combine object binding patterns
580
- if (ts.isVariableStatement(node) &&
581
- ts.isVariableDeclaration(node.declarationList.declarations[0]) &&
582
- ts.isIdentifier(node.declarationList.declarations[0].name) &&
583
- node.declarationList.declarations[0].name.getText() === variable &&
584
- ts.isCallExpression(node.declarationList.declarations[0].initializer) &&
585
- node.declarationList.declarations[0].initializer.expression.getText() ===
586
- 'require' &&
587
- ts.isStringLiteral(node.declarationList.declarations[0].initializer.arguments[0]) &&
588
- node.declarationList.declarations[0].initializer.arguments[0].text === imp) {
579
+ if (!ts.isVariableStatement(node) ||
580
+ !ts.isVariableDeclaration(node.declarationList.declarations[0])) {
581
+ return;
582
+ }
583
+ const declaration = node.declarationList.declarations[0];
584
+ // Match both `const x = require(imp)` and a sole-binding
585
+ // `const { x } = require(imp)`, so an object-binding import is removed too.
586
+ const name = declaration.name;
587
+ const bindsVariable = (ts.isIdentifier(name) && name.getText() === variable) ||
588
+ (ts.isObjectBindingPattern(name) &&
589
+ name.elements.length === 1 &&
590
+ name.elements[0].name.getText() === variable);
591
+ if (bindsVariable &&
592
+ ts.isCallExpression(declaration.initializer) &&
593
+ declaration.initializer.expression.getText() === 'require' &&
594
+ ts.isStringLiteral(declaration.initializer.arguments[0]) &&
595
+ declaration.initializer.arguments[0].text === imp) {
589
596
  changes.push({
590
597
  type: devkit_1.ChangeType.Delete,
591
598
  start: node.pos,
@@ -4,6 +4,7 @@ exports.default = update;
4
4
  const devkit_1 = require("@nx/devkit");
5
5
  const config_file_1 = require("../../utils/config-file");
6
6
  const generator_1 = require("../../generators/convert-to-flat-config/generator");
7
+ const angular_eslint_1 = require("../../generators/convert-to-flat-config/angular-eslint");
7
8
  // Output formatters ESLint removed in v9. Built-in names only; community
8
9
  // formatter packages (referenced by their package name) keep working.
9
10
  const REMOVED_FORMATTERS = new Set([
@@ -51,16 +52,24 @@ async function update(tree) {
51
52
  const removedFormatterTargets = findRemovedFormatterTargets(tree);
52
53
  const unsupportedOptionTargets = findUnsupportedFlatConfigOptionTargets(tree);
53
54
  const rootState = detectRootConfigState(tree);
54
- if (rootState === 'none') {
55
- // No ESLint configuration to migrate.
56
- return;
57
- }
58
55
  if (rootState === 'convertible') {
56
+ // The generator reconciles angular-eslint v22's removed configs as part of
57
+ // conversion, so the converted flat configs load without agent repair.
59
58
  await (0, generator_1.convertToFlatConfigGenerator)(tree, {
60
59
  keepExistingVersions: true,
61
- skipFormat: false,
60
+ skipFormat: true,
62
61
  });
63
62
  }
63
+ // Reconcile angular-eslint v22's removed eslintrc configs in any flat config.
64
+ // Runs for every root state: project-level flat configs can exist even when the
65
+ // root is JavaScript-based (so the generator never ran), already flat, or
66
+ // absent. No-op for the converted root above (idempotent).
67
+ await (0, angular_eslint_1.migrateAngularEslintV22FlatConfig)(tree);
68
+ await (0, devkit_1.formatFiles)(tree);
69
+ if (rootState === 'none') {
70
+ // Nothing to migrate beyond the angular-eslint reconciliation above.
71
+ return;
72
+ }
64
73
  const agentContext = [
65
74
  passingBaselineInstruction(userExplicitRules),
66
75
  ];
@@ -179,7 +179,9 @@ The set of rules the user explicitly configured before the migration is in `<adv
179
179
  nx run-many -t lint
180
180
  ```
181
181
 
182
- 2. For each rule that now reports errors:
182
+ 2. Tell a rule violation apart from a plugin crash. If a project fails with a thrown error instead of rule findings - a `TypeError` such as `context.getAncestors is not a function`, a `Could not find "<rule>" in plugin "<name>"` / `couldn't find the config "<name>" to extend from`, or a plugin that fails to load - the plugin predates ESLint v9; this is not a changed preset default. Do NOT disable the rule (that silently drops its coverage); update the plugin instead (section 6), then re-run lint and continue.
183
+
184
+ 3. For each rule that now reports errors (findings, not a thrown error):
183
185
  - 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
186
  - 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
187
 
@@ -200,6 +202,7 @@ export default [
200
202
  **Action items**:
201
203
 
202
204
  - [ ] Run lint and collect every newly reported rule.
205
+ - [ ] Treat a plugin crash (a thrown error, not rule findings) as a version incompatibility: update the plugin (section 6), never disable its rules to silence it.
203
206
  - [ ] Disable preset-originated rules that the user did not configure.
204
207
  - [ ] Never disable or weaken a rule the user explicitly configured.
205
208
  - [ ] Never edit source files to satisfy a newly enabled rule.
@@ -240,6 +243,7 @@ npm install --save-dev eslint-formatter-junit
240
243
 
241
244
  - **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
245
  - **No eslintrc auto-merge**: flat config does not merge `.eslintrc.*` files found up the tree. Every setting must live in `eslint.config.*`.
246
+ - **Third-party plugins that predate ESLint v9**: an installed ESLint plugin that was not updated for v9 breaks at lint time - its rules call the removed `context` APIs below, or it only ships an eslintrc config that no longer loads. This surfaces as a thrown error (see section 4), not a new rule violation. List the installed plugins from `package.json` (`dependencies`/`devDependencies` matching `eslint-plugin-*` or `@<scope>/eslint-plugin-*`), and for each confirm its version supports ESLint v9 (its changelog, or that `peerDependencies.eslint` allows `>=9`). Update any that do not, and prefer the plugin's flat entry point where it ships one (for example `eslint-plugin-cypress/flat`). Update the plugin rather than disabling its rules.
243
247
  - **Local rule API moved to `SourceCode`** (only relevant if the workspace authors its own rules):
244
248
  - `context.getScope()` to `sourceCode.getScope(node)`
245
249
  - `context.getAncestors()` to `sourceCode.getAncestors(node)`
@@ -252,6 +256,7 @@ npm install --save-dev eslint-formatter-junit
252
256
  **Action items**:
253
257
 
254
258
  - [ ] Remove unsupported CLI flags and executor options; move targeting into `files` / `ignores`.
259
+ - [ ] Update any installed third-party ESLint plugin that does not yet support ESLint v9, preferring its flat entry point; do not disable its rules to work around a load error.
255
260
  - [ ] Update local rules to the `SourceCode` API and add `meta.schema` where required.
256
261
 
257
262
  ---
@@ -7,6 +7,20 @@ config that still references it fails to load. It was split into three rules:
7
7
  - `@typescript-eslint/no-unsafe-function-type` - the `Function` type
8
8
  - `@typescript-eslint/no-wrapper-object-types` - wrapper types (`String`, `Number`, `Boolean`, `Object`, ...)
9
9
 
10
+ ## First, check whether there is anything to do
11
+
12
+ Confirm both conditions before changing anything. If either fails, make no
13
+ changes and stop:
14
+
15
+ 1. Some ESLint flat config (`eslint.config.{mjs,cjs,js,cts,ts,mts}`) references
16
+ `@typescript-eslint/ban-types`. Search the workspace for that string; if no
17
+ config uses the rule, there is nothing to migrate.
18
+ 2. The workspace is on typescript-eslint v8 or later (check `typescript-eslint`
19
+ or `@typescript-eslint/eslint-plugin` in `package.json`). On v7 the rule
20
+ still exists, so leave it untouched.
21
+
22
+ ## Migrate
23
+
10
24
  In every ESLint flat config (`eslint.config.{mjs,cjs,js,cts,ts,mts}`) that sets
11
25
  `@typescript-eslint/ban-types`, replace that single entry with the three rules
12
26
  above. The options do not map 1:1: if the old entry was just `'error'`/`'warn'`,
@@ -3,10 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = update;
4
4
  const tslib_1 = require("tslib");
5
5
  const devkit_1 = require("@nx/devkit");
6
+ const internal_1 = require("@nx/devkit/internal");
7
+ const semver_1 = require("semver");
6
8
  const ts = tslib_1.__importStar(require("typescript"));
7
9
  // Inlined rather than imported from the @nx/eslint utils so this migration stays
8
10
  // self-contained - a migration should not depend on a shared list that can change
9
- // in a later version.
11
+ // in a later version. Includes the shared `eslint.base.config.*` files: they are
12
+ // flat configs too and can carry the removed rules a project config inherits.
10
13
  const ESLINT_FLAT_CONFIG_FILENAMES = [
11
14
  'eslint.config.cjs',
12
15
  'eslint.config.js',
@@ -14,6 +17,14 @@ const ESLINT_FLAT_CONFIG_FILENAMES = [
14
17
  'eslint.config.cts',
15
18
  'eslint.config.ts',
16
19
  'eslint.config.mts',
20
+ 'eslint.base.js',
21
+ 'eslint.base.ts',
22
+ 'eslint.base.config.cjs',
23
+ 'eslint.base.config.js',
24
+ 'eslint.base.config.mjs',
25
+ 'eslint.base.config.cts',
26
+ 'eslint.base.config.ts',
27
+ 'eslint.base.config.mts',
17
28
  ];
18
29
  // Formatting/extension rules typescript-eslint removed in v8 (moved to
19
30
  // @stylistic). A flat config that still references one fails to load: "Could not
@@ -67,7 +78,21 @@ const RENAMED_TS_ESLINT_RULES = new Map([
67
78
  '@typescript-eslint/no-unnecessary-template-expression',
68
79
  ],
69
80
  ]);
81
+ // The stripped rules only disappear in typescript-eslint v8. On v7 (or with
82
+ // typescript-eslint absent) they are still valid, so editing a config would
83
+ // wrongly drop a live rule. Gate on the umbrella `typescript-eslint` or the
84
+ // scoped `@typescript-eslint/eslint-plugin` (a workspace declares one or the
85
+ // other); the migration's JSON `requires` cannot express that OR.
86
+ function hasTypescriptEslintV8(tree) {
87
+ return ['typescript-eslint', '@typescript-eslint/eslint-plugin'].some((pkg) => {
88
+ const version = (0, internal_1.getDeclaredPackageVersion)(tree, pkg);
89
+ return version !== null && (0, semver_1.major)(version) >= 8;
90
+ });
91
+ }
70
92
  async function update(tree) {
93
+ if (!hasTypescriptEslintV8(tree)) {
94
+ return;
95
+ }
71
96
  let changed = false;
72
97
  (0, devkit_1.visitNotIgnoredFiles)(tree, '.', (path) => {
73
98
  const fileName = path.split('/').pop();
package/migrations.json CHANGED
@@ -27,17 +27,11 @@
27
27
  },
28
28
  "update-23-1-0-remove-removed-typescript-eslint-extension-rules": {
29
29
  "version": "23.1.0-beta.5",
30
- "requires": {
31
- "typescript-eslint": ">=8.0.0"
32
- },
33
30
  "description": "Handle typescript-eslint rules removed in v8 in ESLint flat configs, since referencing a removed rule stops the config from loading. Deletes the removed formatting/extension rules (e.g. @typescript-eslint/no-extra-semi) and rewrites the safe 1:1 renames (no-throw-literal -> only-throw-error, no-useless-template-literals -> no-unnecessary-template-expression) to preserve enforcement.",
34
31
  "implementation": "./dist/src/migrations/update-23-1-0/remove-removed-typescript-eslint-extension-rules"
35
32
  },
36
33
  "update-23-1-0-migrate-ban-types-rule": {
37
34
  "version": "23.1.0-beta.5",
38
- "requires": {
39
- "typescript-eslint": ">=8.0.0"
40
- },
41
35
  "description": "Migrate the removed @typescript-eslint/ban-types rule to its v8 successors (no-empty-object-type, no-unsafe-function-type, no-wrapper-object-types), whose options do not map 1:1 - so it is driven by an AI prompt rather than a deterministic codemod.",
42
36
  "prompt": "./dist/src/migrations/update-23-1-0/migrate-ban-types-rule.md"
43
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nx/eslint",
3
- "version": "23.1.0-beta.5",
3
+ "version": "23.1.0-beta.6",
4
4
  "private": false,
5
5
  "type": "commonjs",
6
6
  "files": [
@@ -73,17 +73,17 @@
73
73
  "peerDependencies": {
74
74
  "@zkochan/js-yaml": "0.0.7",
75
75
  "eslint": "^9.0.0 || ^10.0.0",
76
- "@nx/jest": "23.1.0-beta.5"
76
+ "@nx/jest": "23.1.0-beta.6"
77
77
  },
78
78
  "dependencies": {
79
79
  "semver": "^7.6.3",
80
80
  "tslib": "^2.3.0",
81
81
  "typescript": "~6.0.3",
82
- "@nx/devkit": "23.1.0-beta.5",
83
- "@nx/js": "23.1.0-beta.5"
82
+ "@nx/devkit": "23.1.0-beta.6",
83
+ "@nx/js": "23.1.0-beta.6"
84
84
  },
85
85
  "devDependencies": {
86
- "nx": "23.1.0-beta.5"
86
+ "nx": "23.1.0-beta.6"
87
87
  },
88
88
  "peerDependenciesMeta": {
89
89
  "@nx/jest": {