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

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