@nx/eslint 23.1.0-beta.4 → 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.
- package/dist/src/generators/convert-to-flat-config/angular-eslint.d.ts +20 -0
- package/dist/src/generators/convert-to-flat-config/angular-eslint.js +410 -0
- package/dist/src/generators/convert-to-flat-config/generator.js +50 -12
- package/dist/src/generators/init/init.js +26 -13
- package/dist/src/generators/utils/flat-config/ast-utils.js +17 -10
- package/dist/src/generators/workspace-rules-project/workspace-rules-project.js +18 -2
- package/dist/src/migrations/update-21-6-0/update-executor-lint-inputs.js +6 -3
- package/dist/src/migrations/update-23-1-0/convert-to-flat-config.js +22 -5
- package/dist/src/migrations/update-23-1-0/convert-to-flat-config.md +6 -1
- package/dist/src/migrations/update-23-1-0/migrate-ban-types-rule.md +29 -0
- package/dist/src/migrations/update-23-1-0/remove-removed-typescript-eslint-extension-rules.d.ts +2 -0
- package/dist/src/migrations/update-23-1-0/remove-removed-typescript-eslint-extension-rules.js +153 -0
- package/migrations.json +10 -0
- package/package.json +5 -5
|
@@ -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
|
+
}
|
|
@@ -9,6 +9,7 @@ const assert_supported_eslint_version_1 = require("../../utils/assert-supported-
|
|
|
9
9
|
const versions_1 = require("../../utils/versions");
|
|
10
10
|
const config_file_1 = require("../../utils/config-file");
|
|
11
11
|
const json_converter_1 = require("./converters/json-converter");
|
|
12
|
+
const angular_eslint_1 = require("./angular-eslint");
|
|
12
13
|
async function convertToFlatConfigGenerator(tree, options) {
|
|
13
14
|
(0, assert_supported_eslint_version_1.assertSupportedEslintVersion)(tree);
|
|
14
15
|
// Already on flat config at the root? There is nothing to convert.
|
|
@@ -42,7 +43,10 @@ async function convertToFlatConfigGenerator(tree, options) {
|
|
|
42
43
|
// replace references in nx.json and project.json files
|
|
43
44
|
updateNxJsonConfig(tree, options.eslintConfigFormat);
|
|
44
45
|
updateProjectConfigsInputs(tree, options.eslintConfigFormat);
|
|
45
|
-
//
|
|
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);
|
|
46
50
|
if (!options.skipFormat) {
|
|
47
51
|
await (0, devkit_1.formatFiles)(tree);
|
|
48
52
|
}
|
|
@@ -65,6 +69,25 @@ function isEslintTarget(target) {
|
|
|
65
69
|
return (target.executor === ESLINT_LINT_EXECUTOR ||
|
|
66
70
|
target.command?.includes('eslint'));
|
|
67
71
|
}
|
|
72
|
+
function hasMatchingEslintTargetDefault(projectConfig, targetDefaults) {
|
|
73
|
+
if (!projectConfig.targets || !targetDefaults) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return Object.entries(targetDefaults).some(([targetName, value]) => {
|
|
77
|
+
if (projectConfig.targets[targetName] === undefined) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
if (targetName === ESLINT_LINT_EXECUTOR) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
// A target default value can be a plain config object or an array of
|
|
84
|
+
// filtered entries; match against the filter-less (catch-all) entry.
|
|
85
|
+
const targetConfig = Array.isArray(value)
|
|
86
|
+
? value.find((e) => e.filter === undefined)
|
|
87
|
+
: value;
|
|
88
|
+
return targetConfig ? isEslintTarget(targetConfig) : false;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
68
91
|
function convertProjectToFlatConfig(tree, project, projectConfig, nxJson, eslintIgnoreFiles, format, keepExistingVersions) {
|
|
69
92
|
const eslintFile = (0, eslint_file_1.findEslintFile)(tree, projectConfig.root);
|
|
70
93
|
if (!eslintFile) {
|
|
@@ -95,10 +118,7 @@ function convertProjectToFlatConfig(tree, project, projectConfig, nxJson, eslint
|
|
|
95
118
|
if (eslintTargets.length > 0) {
|
|
96
119
|
(0, devkit_1.updateProjectConfiguration)(tree, project, projectConfig);
|
|
97
120
|
}
|
|
98
|
-
const hasEslintTargetDefaults = projectConfig.
|
|
99
|
-
Object.keys(nxJson.targetDefaults || {}).some((t) => (t === ESLINT_LINT_EXECUTOR ||
|
|
100
|
-
isEslintTarget(nxJson.targetDefaults[t])) &&
|
|
101
|
-
projectConfig.targets[t]);
|
|
121
|
+
const hasEslintTargetDefaults = hasMatchingEslintTargetDefault(projectConfig, nxJson.targetDefaults);
|
|
102
122
|
if (eslintTargets.length === 0 &&
|
|
103
123
|
!hasEslintTargetDefaults &&
|
|
104
124
|
!(0, plugin_1.hasEslintPlugin)(tree)) {
|
|
@@ -148,20 +168,33 @@ function ensureInputPresent(inputs, value, format) {
|
|
|
148
168
|
}
|
|
149
169
|
// Updates nx.json: rewrites stale eslintrc/eslintignore references across all targetDefaults
|
|
150
170
|
// inputs and namedInputs, and ensures lint targets include the new flat config file as an input
|
|
151
|
-
// (and `production` excludes it).
|
|
171
|
+
// (and `production` excludes it). Handles both the legacy record shape and the new array shape
|
|
172
|
+
// of `targetDefaults`.
|
|
152
173
|
function updateNxJsonConfig(tree, format) {
|
|
153
174
|
if (!tree.exists('nx.json')) {
|
|
154
175
|
return;
|
|
155
176
|
}
|
|
156
177
|
(0, devkit_1.updateJson)(tree, 'nx.json', (json) => {
|
|
178
|
+
const rewriteTargetInputs = (target, isLintTarget) => {
|
|
179
|
+
if (!target.inputs)
|
|
180
|
+
return;
|
|
181
|
+
target.inputs = isLintTarget
|
|
182
|
+
? ensureInputPresent(target.inputs, `{workspaceRoot}/eslint.config.${format}`, format)
|
|
183
|
+
: rewriteLegacyInputs(target.inputs, format);
|
|
184
|
+
};
|
|
157
185
|
if (json.targetDefaults) {
|
|
158
|
-
for (const [name,
|
|
159
|
-
if (!target.inputs)
|
|
160
|
-
continue;
|
|
186
|
+
for (const [name, value] of Object.entries(json.targetDefaults)) {
|
|
161
187
|
const isLintTarget = name === 'lint' || name === ESLINT_LINT_EXECUTOR;
|
|
162
|
-
target
|
|
163
|
-
|
|
164
|
-
|
|
188
|
+
// A target default value can be a plain config object or an array of
|
|
189
|
+
// filtered entries; rewrite inputs on each entry in the array case.
|
|
190
|
+
if (Array.isArray(value)) {
|
|
191
|
+
for (const entry of value) {
|
|
192
|
+
rewriteTargetInputs(entry, isLintTarget);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
rewriteTargetInputs(value, isLintTarget);
|
|
197
|
+
}
|
|
165
198
|
}
|
|
166
199
|
}
|
|
167
200
|
if (json.namedInputs) {
|
|
@@ -256,6 +289,11 @@ function processConvertedConfig(tree, root, source, target, { content, addESLint
|
|
|
256
289
|
if (addESLintJS) {
|
|
257
290
|
devDependencies['@eslint/js'] = versions_1.eslintVersion;
|
|
258
291
|
}
|
|
292
|
+
// The flat/angular presets import the umbrella `angular-eslint` package; add
|
|
293
|
+
// it when the converted config references them so the result resolves.
|
|
294
|
+
if (content.includes('flat/angular')) {
|
|
295
|
+
devDependencies['angular-eslint'] = (0, angular_eslint_1.resolveAngularEslintVersion)(tree);
|
|
296
|
+
}
|
|
259
297
|
// Direct invocation is an opt-in upgrade, so by default existing pins are
|
|
260
298
|
// overwritten to land the workspace on the latest flat-config-ready stack.
|
|
261
299
|
// Migrations pass `keepExistingVersions` so the version bump stays owned by
|
|
@@ -22,19 +22,32 @@ function updateProductionFileset(tree, format = 'mjs') {
|
|
|
22
22
|
(0, devkit_1.updateNxJson)(tree, nxJson);
|
|
23
23
|
}
|
|
24
24
|
function addTargetDefaults(tree, format) {
|
|
25
|
-
const nxJson = (0, devkit_1.readNxJson)(tree);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
nxJson.targetDefaults
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
const nxJson = (0, devkit_1.readNxJson)(tree) ?? {};
|
|
26
|
+
// `@nx/eslint:lint` is an executor identifier — match defaults keyed on
|
|
27
|
+
// the executor, not on a target named that string.
|
|
28
|
+
const existing = (0, internal_1.findTargetDefault)(nxJson.targetDefaults, {
|
|
29
|
+
executor: '@nx/eslint:lint',
|
|
30
|
+
});
|
|
31
|
+
const patch = {};
|
|
32
|
+
if (existing?.cache === undefined)
|
|
33
|
+
patch.cache = true;
|
|
34
|
+
if (existing?.inputs === undefined) {
|
|
35
|
+
patch.inputs = [
|
|
36
|
+
'default',
|
|
37
|
+
'^default',
|
|
38
|
+
`{workspaceRoot}/.eslintrc.json`,
|
|
39
|
+
`{workspaceRoot}/.eslintignore`,
|
|
40
|
+
`{workspaceRoot}/eslint.config.${format}`,
|
|
41
|
+
'{workspaceRoot}/tools/eslint-rules/**/*',
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
if (Object.keys(patch).length > 0) {
|
|
45
|
+
(0, internal_1.upsertTargetDefault)(tree, nxJson, {
|
|
46
|
+
executor: '@nx/eslint:lint',
|
|
47
|
+
...patch,
|
|
48
|
+
});
|
|
49
|
+
(0, devkit_1.updateNxJson)(tree, nxJson);
|
|
50
|
+
}
|
|
38
51
|
}
|
|
39
52
|
function updateVsCodeRecommendedExtensions(host) {
|
|
40
53
|
if (!host.exists('.vscode/extensions.json')) {
|
|
@@ -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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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,
|
|
@@ -38,8 +38,9 @@ async function lintWorkspaceRulesProjectGenerator(tree, options = {}) {
|
|
|
38
38
|
* TODO: Explore writing a ProjectGraph plugin to make this more surgical.
|
|
39
39
|
*/
|
|
40
40
|
const nxJson = (0, devkit_1.readNxJson)(tree);
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const lintEntry = findLintTargetDefault(nxJson.targetDefaults);
|
|
42
|
+
if (lintEntry?.inputs) {
|
|
43
|
+
lintEntry.inputs.push(`{workspaceRoot}/${exports.WORKSPACE_PLUGIN_DIR}/**/*`);
|
|
43
44
|
(0, devkit_1.updateNxJson)(tree, nxJson);
|
|
44
45
|
}
|
|
45
46
|
// Add jest to the project and return installation task
|
|
@@ -92,3 +93,18 @@ async function lintWorkspaceRulesProjectGenerator(tree, options = {}) {
|
|
|
92
93
|
}
|
|
93
94
|
return (0, devkit_1.runTasksInSerial)(...tasks);
|
|
94
95
|
}
|
|
96
|
+
function findLintTargetDefault(td) {
|
|
97
|
+
const value = td?.['lint'];
|
|
98
|
+
if (value === undefined)
|
|
99
|
+
return undefined;
|
|
100
|
+
// A target default value can be a plain config object or an array of
|
|
101
|
+
// filtered entries; use the filter-less (catch-all) entry.
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
const found = value.find((e) => e.filter === undefined);
|
|
104
|
+
if (!found)
|
|
105
|
+
return undefined;
|
|
106
|
+
const { filter: _filter, ...rest } = found;
|
|
107
|
+
return rest;
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.default = default_1;
|
|
4
4
|
const devkit_1 = require("@nx/devkit");
|
|
5
|
+
const internal_1 = require("@nx/devkit/internal");
|
|
5
6
|
async function default_1(tree) {
|
|
6
|
-
const nxJson = (0, devkit_1.readNxJson)(tree);
|
|
7
|
+
const nxJson = (0, devkit_1.readNxJson)(tree) ?? {};
|
|
7
8
|
const executor = '@nx/eslint:lint';
|
|
8
|
-
|
|
9
|
+
const existing = (0, internal_1.findTargetDefault)(nxJson.targetDefaults, { executor });
|
|
10
|
+
if (!existing?.inputs) {
|
|
9
11
|
return;
|
|
10
12
|
}
|
|
11
|
-
const inputs =
|
|
13
|
+
const inputs = [...existing.inputs];
|
|
12
14
|
if (!inputs.includes('^default')) {
|
|
13
15
|
// Add after 'default' if present, otherwise at the beginning
|
|
14
16
|
const defaultIndex = inputs.indexOf('default');
|
|
@@ -22,6 +24,7 @@ async function default_1(tree) {
|
|
|
22
24
|
if (!inputs.includes('{workspaceRoot}/tools/eslint-rules/**/*')) {
|
|
23
25
|
inputs.push('{workspaceRoot}/tools/eslint-rules/**/*');
|
|
24
26
|
}
|
|
27
|
+
(0, internal_1.upsertTargetDefault)(tree, nxJson, { executor, inputs });
|
|
25
28
|
(0, devkit_1.updateNxJson)(tree, nxJson);
|
|
26
29
|
await (0, devkit_1.formatFiles)(tree);
|
|
27
30
|
}
|
|
@@ -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:
|
|
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
|
];
|
|
@@ -181,6 +190,10 @@ function findRemovedFormatterTargets(tree) {
|
|
|
181
190
|
if (name !== 'lint' && name !== ESLINT_LINT_EXECUTOR) {
|
|
182
191
|
continue;
|
|
183
192
|
}
|
|
193
|
+
if (Array.isArray(target)) {
|
|
194
|
+
// This migration predates the filtered array value form; values are plain objects here.
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
184
197
|
const optionSets = [[null, target.options], ...Object.entries(target.configurations ?? {})];
|
|
185
198
|
collectRemovedFormatters(`targetDefaults["${name}"]`, optionSets);
|
|
186
199
|
}
|
|
@@ -231,6 +244,10 @@ function findUnsupportedFlatConfigOptionTargets(tree) {
|
|
|
231
244
|
if (name !== 'lint' && name !== ESLINT_LINT_EXECUTOR) {
|
|
232
245
|
continue;
|
|
233
246
|
}
|
|
247
|
+
if (Array.isArray(target)) {
|
|
248
|
+
// This migration predates the filtered array value form; values are plain objects here.
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
234
251
|
const optionSets = [[null, target.options], ...Object.entries(target.configurations ?? {})];
|
|
235
252
|
collect(`targetDefaults["${name}"]`, optionSets, FLAT_CONFIG_UNSUPPORTED_OPTIONS);
|
|
236
253
|
}
|
|
@@ -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.
|
|
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
|
---
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Migrate the removed `@typescript-eslint/ban-types` rule
|
|
2
|
+
|
|
3
|
+
typescript-eslint v8 removed `@typescript-eslint/ban-types`, so an ESLint flat
|
|
4
|
+
config that still references it fails to load. It was split into three rules:
|
|
5
|
+
|
|
6
|
+
- `@typescript-eslint/no-empty-object-type` - the `{}` type
|
|
7
|
+
- `@typescript-eslint/no-unsafe-function-type` - the `Function` type
|
|
8
|
+
- `@typescript-eslint/no-wrapper-object-types` - wrapper types (`String`, `Number`, `Boolean`, `Object`, ...)
|
|
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
|
+
|
|
24
|
+
In every ESLint flat config (`eslint.config.{mjs,cjs,js,cts,ts,mts}`) that sets
|
|
25
|
+
`@typescript-eslint/ban-types`, replace that single entry with the three rules
|
|
26
|
+
above. The options do not map 1:1: if the old entry was just `'error'`/`'warn'`,
|
|
27
|
+
set all three to that level; if it customized `types`/`extendDefaults`, translate
|
|
28
|
+
the intent to whichever successor rule covers each banned type and drop anything
|
|
29
|
+
with no equivalent. Then run `nx run-many -t lint` and confirm the configs load.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = update;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const devkit_1 = require("@nx/devkit");
|
|
6
|
+
const internal_1 = require("@nx/devkit/internal");
|
|
7
|
+
const semver_1 = require("semver");
|
|
8
|
+
const ts = tslib_1.__importStar(require("typescript"));
|
|
9
|
+
// Inlined rather than imported from the @nx/eslint utils so this migration stays
|
|
10
|
+
// self-contained - a migration should not depend on a shared list that can change
|
|
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.
|
|
13
|
+
const ESLINT_FLAT_CONFIG_FILENAMES = [
|
|
14
|
+
'eslint.config.cjs',
|
|
15
|
+
'eslint.config.js',
|
|
16
|
+
'eslint.config.mjs',
|
|
17
|
+
'eslint.config.cts',
|
|
18
|
+
'eslint.config.ts',
|
|
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',
|
|
28
|
+
];
|
|
29
|
+
// Formatting/extension rules typescript-eslint removed in v8 (moved to
|
|
30
|
+
// @stylistic). A flat config that still references one fails to load: "Could not
|
|
31
|
+
// find <rule> in plugin @typescript-eslint".
|
|
32
|
+
//
|
|
33
|
+
// These are DELETED, not rewritten to the base ESLint rule (e.g.
|
|
34
|
+
// `@typescript-eslint/no-extra-semi` -> `no-extra-semi`), on purpose: the base
|
|
35
|
+
// equivalents are themselves deprecated and frozen in ESLint 9 (slated for
|
|
36
|
+
// removal), so resurrecting one just defers the same break to the next ESLint
|
|
37
|
+
// major. And Prettier - which nx workspaces run - already enforces this
|
|
38
|
+
// formatting, so the rules were redundant. The long-term home is @stylistic,
|
|
39
|
+
// which we will not auto-add as a dependency.
|
|
40
|
+
const REMOVED_TS_ESLINT_RULES = new Set([
|
|
41
|
+
'block-spacing',
|
|
42
|
+
'brace-style',
|
|
43
|
+
'comma-dangle',
|
|
44
|
+
'comma-spacing',
|
|
45
|
+
'func-call-spacing',
|
|
46
|
+
'indent',
|
|
47
|
+
'key-spacing',
|
|
48
|
+
'keyword-spacing',
|
|
49
|
+
'lines-around-comment',
|
|
50
|
+
'lines-between-class-members',
|
|
51
|
+
'member-delimiter-style',
|
|
52
|
+
'no-extra-parens',
|
|
53
|
+
'no-extra-semi',
|
|
54
|
+
'object-curly-spacing',
|
|
55
|
+
'padding-line-between-statements',
|
|
56
|
+
'quotes',
|
|
57
|
+
'semi',
|
|
58
|
+
'space-before-blocks',
|
|
59
|
+
'space-before-function-paren',
|
|
60
|
+
'space-infix-ops',
|
|
61
|
+
'type-annotation-spacing',
|
|
62
|
+
].map((rule) => `@typescript-eslint/${rule}`));
|
|
63
|
+
// Semantic rules typescript-eslint renamed 1:1 in v8. Unlike the formatting
|
|
64
|
+
// rules these enforce real behavior, so we rewrite the key to the successor to
|
|
65
|
+
// preserve enforcement rather than drop it.
|
|
66
|
+
//
|
|
67
|
+
// `ban-types` is intentionally NOT here: it split into three rules
|
|
68
|
+
// (no-empty-object-type, no-unsafe-function-type, no-wrapper-object-types) whose
|
|
69
|
+
// options do not map cleanly, so it is handled by the companion prompt-based
|
|
70
|
+
// migration instead.
|
|
71
|
+
const RENAMED_TS_ESLINT_RULES = new Map([
|
|
72
|
+
[
|
|
73
|
+
'@typescript-eslint/no-throw-literal',
|
|
74
|
+
'@typescript-eslint/only-throw-error',
|
|
75
|
+
],
|
|
76
|
+
[
|
|
77
|
+
'@typescript-eslint/no-useless-template-literals',
|
|
78
|
+
'@typescript-eslint/no-unnecessary-template-expression',
|
|
79
|
+
],
|
|
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
|
+
}
|
|
92
|
+
async function update(tree) {
|
|
93
|
+
if (!hasTypescriptEslintV8(tree)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
let changed = false;
|
|
97
|
+
(0, devkit_1.visitNotIgnoredFiles)(tree, '.', (path) => {
|
|
98
|
+
const fileName = path.split('/').pop();
|
|
99
|
+
if (!ESLINT_FLAT_CONFIG_FILENAMES.includes(fileName)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const content = tree.read(path, 'utf-8');
|
|
103
|
+
if (!content?.includes('@typescript-eslint/')) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
|
|
107
|
+
// {start, end, newText} edits, applied high offset -> low so earlier offsets
|
|
108
|
+
// stay valid. Covers both deletes (newText '') and key rewrites uniformly.
|
|
109
|
+
const edits = [];
|
|
110
|
+
const visit = (node) => {
|
|
111
|
+
if (ts.isPropertyAssignment(node) &&
|
|
112
|
+
(ts.isStringLiteral(node.name) ||
|
|
113
|
+
ts.isNoSubstitutionTemplateLiteral(node.name))) {
|
|
114
|
+
const ruleName = node.name.text;
|
|
115
|
+
if (REMOVED_TS_ESLINT_RULES.has(ruleName)) {
|
|
116
|
+
// Remove the whole rule entry, including its leading whitespace and the
|
|
117
|
+
// trailing comma, so the surrounding rules object stays valid.
|
|
118
|
+
let end = node.end;
|
|
119
|
+
const trailingComma = content.slice(end).match(/^\s*,/);
|
|
120
|
+
if (trailingComma) {
|
|
121
|
+
end += trailingComma[0].length;
|
|
122
|
+
}
|
|
123
|
+
edits.push({ start: node.getFullStart(), end, newText: '' });
|
|
124
|
+
}
|
|
125
|
+
else if (RENAMED_TS_ESLINT_RULES.has(ruleName)) {
|
|
126
|
+
// Rewrite only the key, preserving the existing quote style and value.
|
|
127
|
+
const keyStart = node.name.getStart(source);
|
|
128
|
+
const quote = content[keyStart];
|
|
129
|
+
edits.push({
|
|
130
|
+
start: keyStart,
|
|
131
|
+
end: node.name.getEnd(),
|
|
132
|
+
newText: `${quote}${RENAMED_TS_ESLINT_RULES.get(ruleName)}${quote}`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
ts.forEachChild(node, visit);
|
|
137
|
+
};
|
|
138
|
+
visit(source);
|
|
139
|
+
if (edits.length) {
|
|
140
|
+
edits.sort((a, b) => b.start - a.start);
|
|
141
|
+
let updated = content;
|
|
142
|
+
for (const edit of edits) {
|
|
143
|
+
updated =
|
|
144
|
+
updated.slice(0, edit.start) + edit.newText + updated.slice(edit.end);
|
|
145
|
+
}
|
|
146
|
+
tree.write(path, updated);
|
|
147
|
+
changed = true;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
if (changed) {
|
|
151
|
+
await (0, devkit_1.formatFiles)(tree);
|
|
152
|
+
}
|
|
153
|
+
}
|
package/migrations.json
CHANGED
|
@@ -24,6 +24,16 @@
|
|
|
24
24
|
"description": "Convert remaining ESLint configs to flat config for ESLint v9 and keep the workspace lint-passing, disabling rules whose preset defaults changed.",
|
|
25
25
|
"implementation": "./dist/src/migrations/update-23-1-0/convert-to-flat-config",
|
|
26
26
|
"prompt": "./dist/src/migrations/update-23-1-0/convert-to-flat-config.md"
|
|
27
|
+
},
|
|
28
|
+
"update-23-1-0-remove-removed-typescript-eslint-extension-rules": {
|
|
29
|
+
"version": "23.1.0-beta.5",
|
|
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.",
|
|
31
|
+
"implementation": "./dist/src/migrations/update-23-1-0/remove-removed-typescript-eslint-extension-rules"
|
|
32
|
+
},
|
|
33
|
+
"update-23-1-0-migrate-ban-types-rule": {
|
|
34
|
+
"version": "23.1.0-beta.5",
|
|
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.",
|
|
36
|
+
"prompt": "./dist/src/migrations/update-23-1-0/migrate-ban-types-rule.md"
|
|
27
37
|
}
|
|
28
38
|
},
|
|
29
39
|
"packageJsonUpdates": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nx/eslint",
|
|
3
|
-
"version": "23.1.0-beta.
|
|
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.
|
|
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.
|
|
83
|
-
"@nx/js": "23.1.0-beta.
|
|
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.
|
|
86
|
+
"nx": "23.1.0-beta.6"
|
|
87
87
|
},
|
|
88
88
|
"peerDependenciesMeta": {
|
|
89
89
|
"@nx/jest": {
|