@nx/eslint 23.1.0 → 23.1.1

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.
@@ -1,4 +1,4 @@
1
- export { addExtendsToLintConfig, addIgnoresToLintConfig, addOverrideToLintConfig, addPluginsToLintConfig, addPredefinedConfigToFlatLintConfig, findEslintFile, isEslintConfigSupported, lintConfigHasOverride, replaceOverridesInLintConfig, updateOverrideInLintConfig, updateRelativePathsInConfig, } from './src/generators/utils/eslint-file';
1
+ export { addExtendsToLintConfig, addIgnoresToLintConfig, addOverrideToLintConfig, addPluginsToLintConfig, addPredefinedConfigToFlatLintConfig, addTypedLintingToFlatConfig, findEslintFile, inspectTypedLinting, isEslintConfigSupported, isTypedLintingEnabled, lintConfigHasOverride, replaceOverridesInLintConfig, updateOverrideInLintConfig, updateRelativePathsInConfig, } from './src/generators/utils/eslint-file';
2
2
  export { addImportToFlatConfig } from './src/generators/utils/flat-config/ast-utils';
3
3
  export { useFlatConfig } from './src/utils/flat-config';
4
4
  export { javaScriptOverride, typeScriptOverride, } from './src/generators/init/global-eslint-config';
package/dist/internal.js CHANGED
@@ -5,15 +5,18 @@
5
5
  // internal consumers and may change without semver protection. Mirrors
6
6
  // `@nx/devkit/internal`.
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.versions = exports.typescriptESLintVersion = exports.getInstalledEslintVersion = exports.setupRootEsLint = exports.typeScriptOverride = exports.javaScriptOverride = exports.useFlatConfig = exports.addImportToFlatConfig = exports.updateRelativePathsInConfig = exports.updateOverrideInLintConfig = exports.replaceOverridesInLintConfig = exports.lintConfigHasOverride = exports.isEslintConfigSupported = exports.findEslintFile = exports.addPredefinedConfigToFlatLintConfig = exports.addPluginsToLintConfig = exports.addOverrideToLintConfig = exports.addIgnoresToLintConfig = exports.addExtendsToLintConfig = void 0;
8
+ exports.versions = exports.typescriptESLintVersion = exports.getInstalledEslintVersion = exports.setupRootEsLint = exports.typeScriptOverride = exports.javaScriptOverride = exports.useFlatConfig = exports.addImportToFlatConfig = exports.updateRelativePathsInConfig = exports.updateOverrideInLintConfig = exports.replaceOverridesInLintConfig = exports.lintConfigHasOverride = exports.isTypedLintingEnabled = exports.isEslintConfigSupported = exports.inspectTypedLinting = exports.findEslintFile = exports.addTypedLintingToFlatConfig = exports.addPredefinedConfigToFlatLintConfig = exports.addPluginsToLintConfig = exports.addOverrideToLintConfig = exports.addIgnoresToLintConfig = exports.addExtendsToLintConfig = void 0;
9
9
  var eslint_file_1 = require("./src/generators/utils/eslint-file");
10
10
  Object.defineProperty(exports, "addExtendsToLintConfig", { enumerable: true, get: function () { return eslint_file_1.addExtendsToLintConfig; } });
11
11
  Object.defineProperty(exports, "addIgnoresToLintConfig", { enumerable: true, get: function () { return eslint_file_1.addIgnoresToLintConfig; } });
12
12
  Object.defineProperty(exports, "addOverrideToLintConfig", { enumerable: true, get: function () { return eslint_file_1.addOverrideToLintConfig; } });
13
13
  Object.defineProperty(exports, "addPluginsToLintConfig", { enumerable: true, get: function () { return eslint_file_1.addPluginsToLintConfig; } });
14
14
  Object.defineProperty(exports, "addPredefinedConfigToFlatLintConfig", { enumerable: true, get: function () { return eslint_file_1.addPredefinedConfigToFlatLintConfig; } });
15
+ Object.defineProperty(exports, "addTypedLintingToFlatConfig", { enumerable: true, get: function () { return eslint_file_1.addTypedLintingToFlatConfig; } });
15
16
  Object.defineProperty(exports, "findEslintFile", { enumerable: true, get: function () { return eslint_file_1.findEslintFile; } });
17
+ Object.defineProperty(exports, "inspectTypedLinting", { enumerable: true, get: function () { return eslint_file_1.inspectTypedLinting; } });
16
18
  Object.defineProperty(exports, "isEslintConfigSupported", { enumerable: true, get: function () { return eslint_file_1.isEslintConfigSupported; } });
19
+ Object.defineProperty(exports, "isTypedLintingEnabled", { enumerable: true, get: function () { return eslint_file_1.isTypedLintingEnabled; } });
17
20
  Object.defineProperty(exports, "lintConfigHasOverride", { enumerable: true, get: function () { return eslint_file_1.lintConfigHasOverride; } });
18
21
  Object.defineProperty(exports, "replaceOverridesInLintConfig", { enumerable: true, get: function () { return eslint_file_1.replaceOverridesInLintConfig; } });
19
22
  Object.defineProperty(exports, "updateOverrideInLintConfig", { enumerable: true, get: function () { return eslint_file_1.updateOverrideInLintConfig; } });
@@ -6,6 +6,10 @@ interface LintProjectOptions {
6
6
  eslintFilePatterns?: string[];
7
7
  tsConfigPaths?: string[];
8
8
  skipFormat: boolean;
9
+ enableTypedLinting?: boolean;
10
+ /**
11
+ * @deprecated Use `enableTypedLinting` instead. This option will be removed in Nx v24.
12
+ */
9
13
  setParserOptionsProject?: boolean;
10
14
  skipPackageJson?: boolean;
11
15
  unitTestRunner?: string;
@@ -99,7 +99,7 @@ async function lintProjectGeneratorInternal(tree, options) {
99
99
  if (!options.rootProject || projectConfig.root !== '.') {
100
100
  const addDependencyChecks = options.addPackageJsonDependencyChecks ||
101
101
  isBuildableLibraryProject(tree, projectConfig);
102
- createEsLintConfiguration(tree, options, projectConfig, options.setParserOptionsProject, options.rootProject, addDependencyChecks);
102
+ createEsLintConfiguration(tree, options, projectConfig, (0, eslint_file_1.isTypedLintingEnabled)(options), options.rootProject, addDependencyChecks);
103
103
  if (addDependencyChecks) {
104
104
  tasks.push((0, devkit_1.addDependenciesToPackageJson)(tree, {}, { 'jsonc-eslint-parser': versions_1.jsoncEslintParserVersion }, undefined, true));
105
105
  }
@@ -120,7 +120,7 @@ async function lintProjectGeneratorInternal(tree, options) {
120
120
  }
121
121
  return (0, devkit_1.runTasksInSerial)(...tasks);
122
122
  }
123
- function createEsLintConfiguration(tree, options, projectConfig, setParserOptionsProject, rootProject, addDependencyChecks) {
123
+ function createEsLintConfiguration(tree, options, projectConfig, enableTypedLinting, rootProject, addDependencyChecks) {
124
124
  // we are only extending root for non-standalone projects or their complementary e2e apps
125
125
  const extendedRootConfig = rootProject ? undefined : (0, eslint_file_1.findEslintFile)(tree);
126
126
  const pathToRootConfig = extendedRootConfig
@@ -138,22 +138,11 @@ function createEsLintConfiguration(tree, options, projectConfig, setParserOption
138
138
  options.eslintConfigFormat = (0, eslint_file_1.determineEslintConfigFormat)(tree.read(extendedRootConfig, 'utf-8'));
139
139
  }
140
140
  }
141
+ // For flat configs, the typed-linting block is appended as a raw AST node
142
+ // below since `tsconfigRootDir` requires a non-JSON-serializable identifier
143
+ // (`import.meta.dirname` / `__dirname`).
141
144
  const overrides = (0, flat_config_1.useFlatConfig)(tree)
142
- ? // For flat configs, keep generated overrides minimal; only add parserOptions when explicitly requested.
143
- [
144
- ...(setParserOptionsProject
145
- ? [
146
- {
147
- files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
148
- languageOptions: {
149
- parserOptions: {
150
- project: [`${projectConfig.root}/tsconfig.*?.json`],
151
- },
152
- },
153
- },
154
- ]
155
- : []),
156
- ]
145
+ ? []
157
146
  : [
158
147
  {
159
148
  files: ['*.ts', '*.tsx', '*.js', '*.jsx'],
@@ -170,7 +159,7 @@ function createEsLintConfiguration(tree, options, projectConfig, setParserOption
170
159
  * parserOptions.project), the executor will attempt to look for the particular error typescript-eslint gives you
171
160
  * and provide feedback to the user.
172
161
  */
173
- parserOptions: !setParserOptionsProject
162
+ parserOptions: !enableTypedLinting
174
163
  ? undefined
175
164
  : {
176
165
  project: [`${projectConfig.root}/tsconfig.*?.json`],
@@ -214,6 +203,12 @@ function createEsLintConfiguration(tree, options, projectConfig, setParserOption
214
203
  importMap.set(pathToRootConfig, 'baseConfig');
215
204
  nodes.push((0, ast_utils_1.generateSpreadElement)('baseConfig'));
216
205
  }
206
+ // After the base config, never before it: ESLint merges `parserOptions`
207
+ // across entries and the last one wins, so this block's `project: null` only
208
+ // neutralizes a `project` the base config sets while it comes later.
209
+ if (enableTypedLinting) {
210
+ nodes.push((0, ast_utils_1.generateTypedLintingFlatConfigOverride)(options.eslintConfigFormat));
211
+ }
217
212
  overrides.forEach((override) => {
218
213
  nodes.push((0, ast_utils_1.generateFlatOverride)(override, options.eslintConfigFormat));
219
214
  });
@@ -4,6 +4,61 @@ export declare function findEslintFile(tree: Tree, projectRoot?: string): string
4
4
  export declare function isEslintConfigSupported(tree: Tree, projectRoot?: string): boolean;
5
5
  export declare function updateRelativePathsInConfig(tree: Tree, sourcePath: string, destinationPath: string): void;
6
6
  export declare function determineEslintConfigFormat(content: string): 'mjs' | 'cjs';
7
+ /**
8
+ * Honors both `enableTypedLinting` and the deprecated `setParserOptionsProject`
9
+ * (slated for removal in Nx v24); either one truthy enables typed linting. A
10
+ * generator whose `enableTypedLinting` schema default is `false` must still
11
+ * honor a user who set the deprecated flag.
12
+ */
13
+ export declare function isTypedLintingEnabled(options: {
14
+ enableTypedLinting?: boolean;
15
+ setParserOptionsProject?: boolean;
16
+ }): boolean;
17
+ /**
18
+ * What a config says about typed linting.
19
+ *
20
+ * Only keys inside a `parserOptions` object count, so an unrelated `project`
21
+ * (e.g. `settings['import/resolver'].typescript.project`) is not a false match.
22
+ * A local array the config spreads in is read too, since ESLint merges those
23
+ * entries in as if they were inline.
24
+ */
25
+ export declare function inspectTypedLinting(content: string): TypedLintingReport;
26
+ /**
27
+ * What a config configures for typed linting, read from its `parserOptions`
28
+ * (a local array it spreads in included, since ESLint merges those entries).
29
+ */
30
+ export interface TypedLintingReport {
31
+ /**
32
+ * Typed-linting parser options are set at all, an explicit
33
+ * `projectService: false` opt-out included, since overwriting that would
34
+ * undo a deliberate choice.
35
+ */
36
+ own: boolean;
37
+ /**
38
+ * The project service is on, which covers every file the config applies to
39
+ * whichever tsconfig each one belongs to.
40
+ */
41
+ projectService: boolean;
42
+ /** A `project` typescript-eslint builds a program from is set. */
43
+ project: boolean;
44
+ /**
45
+ * A local `parserOptions` is present but set through an expression we can't
46
+ * read statically (a call, an imported reference, a dynamic key), so whether
47
+ * it configures typed linting is unknown. Distinct from a config that merely
48
+ * spreads in another file (no local `parserOptions`), which stays safe to
49
+ * append to.
50
+ */
51
+ uncertain: boolean;
52
+ }
53
+ /**
54
+ * Adds a typed-linting block (`parserOptions.projectService` + `tsconfigRootDir`)
55
+ * to a project's flat ESLint config. No-op for legacy `.eslintrc` configs, whose
56
+ * JSON format cannot express the `__dirname` that `tsconfigRootDir` needs.
57
+ *
58
+ * Use after operations that strip existing overrides (e.g.
59
+ * `replaceOverridesInLintConfig`) to re-establish typed linting.
60
+ */
61
+ export declare function addTypedLintingToFlatConfig(tree: Tree, root: string): void;
7
62
  export declare function addOverrideToLintConfig(tree: Tree, root: string, override: Partial<Linter.ConfigOverride<Linter.RulesRecord>>, options?: {
8
63
  insertAtTheEnd?: boolean;
9
64
  checkBaseConfig?: boolean;
@@ -4,6 +4,9 @@ exports.findEslintFile = findEslintFile;
4
4
  exports.isEslintConfigSupported = isEslintConfigSupported;
5
5
  exports.updateRelativePathsInConfig = updateRelativePathsInConfig;
6
6
  exports.determineEslintConfigFormat = determineEslintConfigFormat;
7
+ exports.isTypedLintingEnabled = isTypedLintingEnabled;
8
+ exports.inspectTypedLinting = inspectTypedLinting;
9
+ exports.addTypedLintingToFlatConfig = addTypedLintingToFlatConfig;
7
10
  exports.addOverrideToLintConfig = addOverrideToLintConfig;
8
11
  exports.updateOverrideInLintConfig = updateOverrideInLintConfig;
9
12
  exports.lintConfigHasOverride = lintConfigHasOverride;
@@ -128,11 +131,689 @@ function offsetFilePath(projectRoot, pathToFile, offset, tree) {
128
131
  }
129
132
  return (0, devkit_1.joinPathFragments)(offset, projectRoot, pathToFile);
130
133
  }
134
+ /**
135
+ * The module system a flat config file actually runs under. `.cts` and `.mts`
136
+ * fix it by extension, so the content is only a signal for the ambiguous ones
137
+ * (`.js`, `.ts`). Trusting content alone reads an idiomatic `export default` in
138
+ * a `.cts` as ESM and emits an `import.meta` its CommonJS output rejects.
139
+ */
140
+ function determineEslintConfigFormatForFile(fileName, content) {
141
+ const extension = (0, posix_1.extname)(fileName);
142
+ if (extension === '.mjs' || extension === '.mts') {
143
+ return 'mjs';
144
+ }
145
+ if (extension === '.cjs' || extension === '.cts') {
146
+ return 'cjs';
147
+ }
148
+ return determineEslintConfigFormat(content);
149
+ }
131
150
  function determineEslintConfigFormat(content) {
132
151
  const sourceFile = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true);
133
- // Check if there's an `export default` in the AST
134
- const hasExportDefault = sourceFile.statements.some((statement) => ts.isExportAssignment(statement) && !statement.isExportEquals);
135
- return hasExportDefault ? 'mjs' : 'cjs';
152
+ return (0, ast_utils_1.isEsmExport)(sourceFile) ? 'mjs' : 'cjs';
153
+ }
154
+ /**
155
+ * Honors both `enableTypedLinting` and the deprecated `setParserOptionsProject`
156
+ * (slated for removal in Nx v24); either one truthy enables typed linting. A
157
+ * generator whose `enableTypedLinting` schema default is `false` must still
158
+ * honor a user who set the deprecated flag.
159
+ */
160
+ function isTypedLintingEnabled(options) {
161
+ return !!(options.enableTypedLinting || options.setParserOptionsProject);
162
+ }
163
+ /**
164
+ * What a config says about typed linting.
165
+ *
166
+ * Only keys inside a `parserOptions` object count, so an unrelated `project`
167
+ * (e.g. `settings['import/resolver'].typescript.project`) is not a false match.
168
+ * A local array the config spreads in is read too, since ESLint merges those
169
+ * entries in as if they were inline.
170
+ */
171
+ function inspectTypedLinting(content) {
172
+ const readings = findParserOptions(content);
173
+ const blocks = readings.filter(isReadParserOptions);
174
+ const own = blocks.some((block) => block.projectService !== 'absent' || block.enablesProject);
175
+ return {
176
+ own,
177
+ projectService: blocks.some((block) => block.projectService === 'enabled'),
178
+ project: blocks.some((block) => block.enablesProject),
179
+ // A local `parserOptions` set through an expression we can't read leaves
180
+ // typed linting undecided: appending would risk converting a `project`
181
+ // setup to the project service, so callers warn and skip. A definite setting
182
+ // (own) already decides the outcome, so it takes precedence.
183
+ uncertain: !own && readings.some((reading) => reading === UNREADABLE),
184
+ };
185
+ }
186
+ /**
187
+ * A `parserOptions` present in a config position whose value can't be read
188
+ * statically. It carries no settings, so it can't be a `ParserOptions`; it marks
189
+ * the result undecided rather than reading as no typed linting.
190
+ */
191
+ const UNREADABLE = 'unreadable';
192
+ function isReadParserOptions(reading) {
193
+ return reading !== UNREADABLE;
194
+ }
195
+ /**
196
+ * A legacy config can be JSON, JS or YAML, and a bare `.eslintrc` can be any of
197
+ * them, so each parser is tried in turn rather than picked from the filename.
198
+ * YAML goes last: a one-line JS config can parse as YAML into a mapping keyed on
199
+ * the whole line, which would otherwise preempt the parser that understands it.
200
+ */
201
+ function findParserOptions(content) {
202
+ const eslintrc = tryParseJson(content);
203
+ if (eslintrc) {
204
+ return findParserOptionsInJson(eslintrc);
205
+ }
206
+ const blocks = findParserOptionsInSource(content);
207
+ if (blocks.length > 0) {
208
+ return blocks;
209
+ }
210
+ const yaml = tryParseYaml(content);
211
+ return yaml ? findParserOptionsInJson(yaml) : blocks;
212
+ }
213
+ function tryParseJson(content) {
214
+ try {
215
+ const parsed = (0, devkit_1.parseJson)(content);
216
+ return typeof parsed === 'object' && parsed !== null ? parsed : null;
217
+ }
218
+ catch {
219
+ return null;
220
+ }
221
+ }
222
+ /**
223
+ * `@zkochan/js-yaml` is an optional peer, so a workspace without it reads a YAML
224
+ * config as no typed linting, which is what happened before YAML was parsed at
225
+ * all.
226
+ */
227
+ function tryParseYaml(content) {
228
+ try {
229
+ const { load } = require('@zkochan/js-yaml');
230
+ const parsed = load(content, { json: true });
231
+ return typeof parsed === 'object' && parsed !== null ? parsed : null;
232
+ }
233
+ catch {
234
+ return null;
235
+ }
236
+ }
237
+ /**
238
+ * A legacy config carries parser options at the top level and in each
239
+ * `overrides` entry, and ESLint rejects a nested `overrides`, so those are the
240
+ * only two places to read. Walking the whole document instead would pick up a
241
+ * `parserOptions` that configures something else, such as one inside a rule's
242
+ * options.
243
+ */
244
+ function findParserOptionsInJson(config) {
245
+ const blocks = [];
246
+ const read = (entry) => {
247
+ const options = entry?.parserOptions;
248
+ if (typeof options === 'object' && options !== null) {
249
+ blocks.push(readJsonParserOptions(options));
250
+ }
251
+ };
252
+ read(config);
253
+ const { overrides } = config;
254
+ if (Array.isArray(overrides)) {
255
+ overrides.forEach(read);
256
+ }
257
+ return blocks;
258
+ }
259
+ function readJsonParserOptions(options) {
260
+ return {
261
+ projectService: !('projectService' in options)
262
+ ? 'absent'
263
+ : isFalsyValue(options.projectService)
264
+ ? 'disabled'
265
+ : 'enabled',
266
+ enablesProject: 'project' in options && !isFalsyValue(options.project),
267
+ };
268
+ }
269
+ function isFalsyValue(value) {
270
+ return value === false || value === null || value === undefined;
271
+ }
272
+ function findParserOptionsInSource(content) {
273
+ const { source, checker } = parseSource(content);
274
+ const configs = findConfigExpressions(source);
275
+ if (configs.length === 0) {
276
+ // No recognizable export (e.g. a `.eslintrc.js` legacy config, or a
277
+ // tokenizer-only fixture): scan the whole source. That can read a
278
+ // `parserOptions` from a declaration the config never uses, but a real flat
279
+ // config exports its array, so the structured walk covers those.
280
+ return collectParserOptions(source, checker, new Set());
281
+ }
282
+ const seen = new Set();
283
+ return configs.flatMap((config) => walkValue(config, checker, seen));
284
+ }
285
+ /**
286
+ * The value each config export denotes: `export default <expr>` (or `export =`)
287
+ * and `module.exports = <expr>`. Only exports declared in this file, so an export
288
+ * assembled in another module names nothing this walk reads.
289
+ */
290
+ function findConfigExpressions(source) {
291
+ const expressions = [];
292
+ for (const statement of source.statements) {
293
+ if (ts.isExportAssignment(statement)) {
294
+ expressions.push(statement.expression);
295
+ }
296
+ else if (ts.isExpressionStatement(statement) &&
297
+ ts.isBinaryExpression(statement.expression) &&
298
+ statement.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
299
+ isModuleExports(statement.expression.left)) {
300
+ expressions.push(statement.expression.right);
301
+ }
302
+ }
303
+ return expressions;
304
+ }
305
+ /** The `module.exports` target of a CommonJS config's top-level assignment. */
306
+ function isModuleExports(node) {
307
+ return (ts.isPropertyAccessExpression(node) &&
308
+ ts.isIdentifier(node.expression) &&
309
+ node.expression.text === 'module' &&
310
+ node.name.text === 'exports');
311
+ }
312
+ const IGNORED_CONFIG_KEYS = new Set(['rules', 'settings', 'plugins']);
313
+ /**
314
+ * A property whose value never holds real parser options, so the walk can skip
315
+ * its subtree. A rule's options, a `settings` value and a `plugins` map can each
316
+ * carry an object with a `parserOptions` key that configures something else;
317
+ * ESLint reads parser options only from `languageOptions`.
318
+ */
319
+ function isIgnoredConfigKey(name) {
320
+ return IGNORED_CONFIG_KEYS.has(getPropertyName(name) ?? '');
321
+ }
322
+ /**
323
+ * Every `parserOptions` reachable from a config value, walking only the
324
+ * positions ESLint reads a config from: array entries, config-object property
325
+ * values (a `rules`/`settings`/`plugins` subtree skipped), object and array
326
+ * spreads, and the arguments of a wrapper call such as `tseslint.config(...)`. A
327
+ * name in any of those positions resolves to its local declaration, so a config
328
+ * assembled from `const` bindings reads the same as an inline one, and a name
329
+ * bound to an import (another file) resolves to nothing. `seen` guards a
330
+ * reference cycle.
331
+ *
332
+ * `inConfigPosition` tracks whether the value itself sits where ESLint reads a
333
+ * config from, as opposed to being an ordinary property value. A wrapper call
334
+ * forwards its arguments as config only in the former; in the latter it is an
335
+ * arbitrary function whose arguments stay unread.
336
+ */
337
+ function walkValue(expression, checker, seen, inConfigPosition = true) {
338
+ const value = resolveExpressionValue(expression, checker, seen);
339
+ if (!value) {
340
+ return [];
341
+ }
342
+ if (ts.isArrayLiteralExpression(value)) {
343
+ // Elements inherit this array's position: a root config array holds config
344
+ // entries, an array under `files`/`ignores` holds ordinary values.
345
+ return value.elements.flatMap((element) => ts.isSpreadElement(element)
346
+ ? walkValue(element.expression, checker, seen, inConfigPosition)
347
+ : walkValue(element, checker, seen, inConfigPosition));
348
+ }
349
+ if (ts.isObjectLiteralExpression(value)) {
350
+ return value.properties.flatMap((property) => walkProperty(property, checker, seen));
351
+ }
352
+ if (ts.isCallExpression(value)) {
353
+ // A wrapper (`tseslint.config(cfg)`) forwards its arguments as config
354
+ // entries, but only when the call itself sits in a config position; as a
355
+ // property value (`files: getFiles({...})`) the call is arbitrary and its
356
+ // arguments are inputs, not config.
357
+ const results = inConfigPosition
358
+ ? value.arguments.flatMap((argument) => ts.isSpreadElement(argument)
359
+ ? walkValue(argument.expression, checker, seen)
360
+ : walkValue(argument, checker, seen))
361
+ : [];
362
+ // An IIFE (`(() => [...])()`) builds the config in the callee body, so read
363
+ // its returns unconditionally (unlike wrapper arguments), at this call's own
364
+ // position.
365
+ const callee = unwrapExpression(value.expression);
366
+ if (ts.isArrowFunction(callee) || ts.isFunctionExpression(callee)) {
367
+ for (const returned of returnedExpressions(callee)) {
368
+ results.push(...walkValue(returned, checker, seen, inConfigPosition));
369
+ }
370
+ }
371
+ return results;
372
+ }
373
+ if (ts.isConditionalExpression(value)) {
374
+ // Either branch of `cond ? a : b` may be the value at runtime, so a setting
375
+ // in either counts. The branches inherit this value's position.
376
+ return [
377
+ ...walkValue(value.whenTrue, checker, seen, inConfigPosition),
378
+ ...walkValue(value.whenFalse, checker, seen, inConfigPosition),
379
+ ];
380
+ }
381
+ if (ts.isBinaryExpression(value) && isShortCircuit(value.operatorToken)) {
382
+ // A short-circuit (`cond && cfg`, `base || [...]`, `base ?? [...]`) resolves
383
+ // to one operand; read both, since either may be the value at runtime.
384
+ return [
385
+ ...walkValue(value.left, checker, seen, inConfigPosition),
386
+ ...walkValue(value.right, checker, seen, inConfigPosition),
387
+ ];
388
+ }
389
+ if (ts.isBinaryExpression(value) &&
390
+ value.operatorToken.kind === ts.SyntaxKind.CommaToken) {
391
+ // A comma expression (`sideEffect(), cfg`) always evaluates to its right
392
+ // operand.
393
+ return walkValue(value.right, checker, seen, inConfigPosition);
394
+ }
395
+ return [];
396
+ }
397
+ /** The `&&`, `||` and `??` operators, whose result is one of their operands. */
398
+ function isShortCircuit(operator) {
399
+ return (operator.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
400
+ operator.kind === ts.SyntaxKind.BarBarToken ||
401
+ operator.kind === ts.SyntaxKind.QuestionQuestionToken);
402
+ }
403
+ /** The values an inline function returns: a concise arrow body, or each `return`. */
404
+ function returnedExpressions(fn) {
405
+ if (!ts.isBlock(fn.body)) {
406
+ return [fn.body];
407
+ }
408
+ const expressions = [];
409
+ const visit = (node) => {
410
+ if (ts.isReturnStatement(node)) {
411
+ if (node.expression) {
412
+ expressions.push(node.expression);
413
+ }
414
+ return;
415
+ }
416
+ // A nested function has its own returns, unrelated to this one's value.
417
+ if (ts.isFunctionLike(node)) {
418
+ return;
419
+ }
420
+ ts.forEachChild(node, visit);
421
+ };
422
+ ts.forEachChild(fn.body, visit);
423
+ return expressions;
424
+ }
425
+ /** The `parserOptions` a single config-object property contributes. */
426
+ function walkProperty(property, checker, seen) {
427
+ if (ts.isSpreadAssignment(property)) {
428
+ return walkValue(property.expression, checker, seen, false);
429
+ }
430
+ const key = property.name ? getPropertyName(property.name) : null;
431
+ if (key && IGNORED_CONFIG_KEYS.has(key)) {
432
+ return [];
433
+ }
434
+ if (key === 'parserOptions') {
435
+ const object = getParserOptionsObject(property, checker);
436
+ return [object ? readParserOptions(object, checker) : UNREADABLE];
437
+ }
438
+ if (ts.isPropertyAssignment(property)) {
439
+ return walkValue(property.initializer, checker, seen, false);
440
+ }
441
+ if (ts.isShorthandPropertyAssignment(property)) {
442
+ // `{ languageOptions }`: follow the shorthand to its declaration and walk it.
443
+ const declaration = checker.getShorthandAssignmentValueSymbol(property)?.declarations?.[0];
444
+ if (declaration &&
445
+ ts.isVariableDeclaration(declaration) &&
446
+ declaration.initializer &&
447
+ !seen.has(declaration)) {
448
+ seen.add(declaration);
449
+ return walkValue(declaration.initializer, checker, seen, false);
450
+ }
451
+ }
452
+ return [];
453
+ }
454
+ /**
455
+ * Export-less fallback: every `parserOptions` reachable from a node, local array
456
+ * spreads followed. A spread of a local array contributes that array's entries;
457
+ * a spread of another module names no file this walk reads.
458
+ */
459
+ function collectParserOptions(root, checker, localSeen) {
460
+ const blocks = [];
461
+ const visit = (node) => {
462
+ if (ts.isPropertyAssignment(node) && isIgnoredConfigKey(node.name)) {
463
+ return;
464
+ }
465
+ if (isParserOptionsProperty(node)) {
466
+ const object = getParserOptionsObject(node, checker);
467
+ blocks.push(object ? readParserOptions(object, checker) : UNREADABLE);
468
+ }
469
+ if (ts.isSpreadElement(node)) {
470
+ blocks.push(...followLocalSpread(node.expression, checker, localSeen));
471
+ }
472
+ ts.forEachChild(node, visit);
473
+ };
474
+ visit(root);
475
+ return blocks;
476
+ }
477
+ /**
478
+ * The expression a name (or an `obj.key` / `obj['key']` on a local object)
479
+ * ultimately denotes, following alias chains such as `const inner = [...]; const
480
+ * base = inner; export default base`. Anything we can't read statically (an
481
+ * import, a dynamic key) resolves to nothing. `seen` guards a cycle like
482
+ * `const a = b; const b = a`.
483
+ */
484
+ function resolveExpressionValue(expression, checker, seen) {
485
+ const unwrapped = unwrapExpression(expression);
486
+ if (ts.isPropertyAccessExpression(unwrapped) ||
487
+ ts.isElementAccessExpression(unwrapped)) {
488
+ const value = resolveMemberValue(unwrapped, checker);
489
+ return value ? resolveExpressionValue(value, checker, seen) : null;
490
+ }
491
+ if (!ts.isIdentifier(unwrapped)) {
492
+ return unwrapped;
493
+ }
494
+ const declaration = checker.getSymbolAtLocation(unwrapped)?.declarations?.[0];
495
+ if (!declaration ||
496
+ !ts.isVariableDeclaration(declaration) ||
497
+ !declaration.initializer ||
498
+ seen.has(declaration)) {
499
+ return null;
500
+ }
501
+ seen.add(declaration);
502
+ return resolveExpressionValue(declaration.initializer, checker, seen);
503
+ }
504
+ /**
505
+ * The value at `obj.key` / `obj['key']` when `obj` resolves to a local object
506
+ * literal and the key is a static string. An imported object, a dynamic key or a
507
+ * missing property resolves to nothing. A fresh `seen` isolates the object lookup
508
+ * so sibling accesses on the same registry don't shadow one another.
509
+ */
510
+ function resolveMemberValue(access, checker) {
511
+ const key = memberKey(access);
512
+ if (key === null) {
513
+ return null;
514
+ }
515
+ const object = resolveObjectExpression(access.expression, checker, new Set());
516
+ if (!object) {
517
+ return null;
518
+ }
519
+ return lookupObjectKey(object, key, checker, new Set()).value;
520
+ }
521
+ /**
522
+ * The value bound to `key` in an object literal, honoring source order (a later
523
+ * property or spread wins, as at runtime) across plain, shorthand and spread
524
+ * properties. `found` separates an absent key (an earlier value stands) from a
525
+ * present but unreadable one (it clears the earlier value); an unreadable spread
526
+ * object can't prove the key is present, so it leaves an earlier value in place.
527
+ * `seen` is the recursion stack, so a spread cycle stops while the same object
528
+ * reached again through a sibling spread still gets read.
529
+ */
530
+ function lookupObjectKey(object, key, checker, seen) {
531
+ if (seen.has(object)) {
532
+ return { found: false, value: null };
533
+ }
534
+ seen.add(object);
535
+ let result = {
536
+ found: false,
537
+ value: null,
538
+ };
539
+ for (const property of object.properties) {
540
+ if (ts.isPropertyAssignment(property)) {
541
+ if (getPropertyName(property.name) === key) {
542
+ result = { found: true, value: property.initializer };
543
+ }
544
+ }
545
+ else if (ts.isShorthandPropertyAssignment(property)) {
546
+ if (property.name.text === key) {
547
+ result = { found: true, value: shorthandValue(property, checker) };
548
+ }
549
+ }
550
+ else if (ts.isSpreadAssignment(property)) {
551
+ const spread = resolveObjectExpression(property.expression, checker, new Set());
552
+ const fromSpread = spread
553
+ ? lookupObjectKey(spread, key, checker, seen)
554
+ : null;
555
+ if (fromSpread && fromSpread.found) {
556
+ result = fromSpread;
557
+ }
558
+ }
559
+ }
560
+ seen.delete(object);
561
+ return result;
562
+ }
563
+ /** The initializer a shorthand property's binding was declared with. */
564
+ function shorthandValue(property, checker) {
565
+ const declaration = checker.getShorthandAssignmentValueSymbol(property)?.declarations?.[0];
566
+ return declaration &&
567
+ ts.isVariableDeclaration(declaration) &&
568
+ declaration.initializer
569
+ ? declaration.initializer
570
+ : null;
571
+ }
572
+ /** The static key of a member access, or `null` for a dynamic one. */
573
+ function memberKey(access) {
574
+ if (ts.isPropertyAccessExpression(access)) {
575
+ return access.name.text;
576
+ }
577
+ const argument = access.argumentExpression;
578
+ if (ts.isStringLiteralLike(argument) || ts.isNumericLiteral(argument)) {
579
+ return argument.text;
580
+ }
581
+ return null;
582
+ }
583
+ /** A local array the config spreads in; its entries belong to this config. */
584
+ function followLocalSpread(expression, checker, localSeen) {
585
+ // Only a name needs following. An inline array is already being walked as
586
+ // part of the surrounding config, so resolving it again would double-count.
587
+ // A name bound to an imported module resolves to nothing here (only a local
588
+ // declaration has an initializer to read), so a spread config from another
589
+ // file contributes nothing and the caller is free to append.
590
+ const unwrapped = unwrapExpression(expression);
591
+ if (!ts.isIdentifier(unwrapped)) {
592
+ return [];
593
+ }
594
+ const value = resolveExpressionValue(expression, checker, localSeen);
595
+ return value ? collectParserOptions(value, checker, localSeen) : [];
596
+ }
597
+ /**
598
+ * Parses the config with a type checker attached, so a `parserOptions` written
599
+ * as a reference resolves under real scope rules (shadowing, destructuring,
600
+ * parameters, imports) rather than an approximation of them. One in-memory
601
+ * file, no lib and no module resolution, so nothing reads from disk.
602
+ */
603
+ function parseSource(content) {
604
+ // Parsed as TS: flat configs may be `.ts`/`.cts`/`.mts`, and TS is a superset,
605
+ // so the same parse covers the `.js` variants.
606
+ const fileName = 'eslint.config.ts';
607
+ const source = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
608
+ const host = {
609
+ getSourceFile: (name) => (name === fileName ? source : undefined),
610
+ getDefaultLibFileName: () => 'lib.d.ts',
611
+ writeFile: () => { },
612
+ getCurrentDirectory: () => '',
613
+ getCanonicalFileName: (name) => name,
614
+ useCaseSensitiveFileNames: () => true,
615
+ getNewLine: () => '\n',
616
+ fileExists: (name) => name === fileName,
617
+ readFile: (name) => (name === fileName ? content : undefined),
618
+ };
619
+ const program = ts.createProgram([fileName], { noLib: true, allowJs: true, types: [] }, host);
620
+ return { source, checker: program.getTypeChecker() };
621
+ }
622
+ /** A `parserOptions` property, whether written in full or by ES shorthand. */
623
+ function isParserOptionsProperty(node) {
624
+ return ((ts.isShorthandPropertyAssignment(node) &&
625
+ node.name.text === 'parserOptions') ||
626
+ (ts.isPropertyAssignment(node) &&
627
+ getPropertyName(node.name) === 'parserOptions'));
628
+ }
629
+ /**
630
+ * The `parserOptions` object a node contributes: an inline literal, or the
631
+ * literal behind a reference (`parserOptions: opts`, or the `{ parserOptions }`
632
+ * shorthand). A variable only counts once the config actually references it, so
633
+ * an unused declaration configures nothing.
634
+ */
635
+ function getParserOptionsObject(node, checker) {
636
+ if (ts.isShorthandPropertyAssignment(node) &&
637
+ node.name.text === 'parserOptions') {
638
+ return resolveSymbolObject(checker.getShorthandAssignmentValueSymbol(node), checker, new Set());
639
+ }
640
+ if (!ts.isPropertyAssignment(node) ||
641
+ getPropertyName(node.name) !== 'parserOptions') {
642
+ return null;
643
+ }
644
+ return resolveObjectExpression(node.initializer, checker, new Set());
645
+ }
646
+ /**
647
+ * Strips the expression wrappers that don't change the value, so an object
648
+ * literal behind `as const`, `satisfies`, a non-null assertion or parentheses is
649
+ * still read as one.
650
+ */
651
+ function unwrapExpression(node) {
652
+ let current = node;
653
+ while (ts.isParenthesizedExpression(current) ||
654
+ ts.isAsExpression(current) ||
655
+ ts.isSatisfiesExpression(current) ||
656
+ ts.isNonNullExpression(current) ||
657
+ ts.isTypeAssertionExpression(current)) {
658
+ current = current.expression;
659
+ }
660
+ return current;
661
+ }
662
+ /**
663
+ * The object literal an expression ultimately denotes, following alias chains
664
+ * (`const opts = typed`) and a static member access on a local object
665
+ * (`registry.opts`). Anything else the name could be bound to (a parameter, a
666
+ * destructured property, a call result, an import, a dynamic key) can't be read
667
+ * statically, so it resolves to nothing and leaves the config alone. `seen`
668
+ * guards against a cycle such as `const a = b; const b = a`.
669
+ */
670
+ function resolveObjectExpression(expression, checker, seen) {
671
+ const unwrapped = unwrapExpression(expression);
672
+ if (ts.isObjectLiteralExpression(unwrapped)) {
673
+ return unwrapped;
674
+ }
675
+ if (ts.isPropertyAccessExpression(unwrapped) ||
676
+ ts.isElementAccessExpression(unwrapped)) {
677
+ const value = resolveMemberValue(unwrapped, checker);
678
+ return value ? resolveObjectExpression(value, checker, seen) : null;
679
+ }
680
+ if (!ts.isIdentifier(unwrapped) || seen.has(unwrapped)) {
681
+ return null;
682
+ }
683
+ seen.add(unwrapped);
684
+ return resolveSymbolObject(checker.getSymbolAtLocation(unwrapped), checker, seen);
685
+ }
686
+ /** The object literal behind a resolved name, if it was declared with one. */
687
+ function resolveSymbolObject(symbol, checker, seen) {
688
+ const declaration = symbol?.declarations?.[0];
689
+ return declaration &&
690
+ ts.isVariableDeclaration(declaration) &&
691
+ declaration.initializer
692
+ ? resolveObjectExpression(declaration.initializer, checker, seen)
693
+ : null;
694
+ }
695
+ /**
696
+ * The properties a `parserOptions` object ends up with, spreads expanded in
697
+ * source order so a later entry overrides an earlier one, as it does at runtime.
698
+ * A value of `null` means the key is set to something we can't read (a shorthand
699
+ * or a nested reference). `seen` is the recursion stack, so a spread cycle stops
700
+ * while the same object spread again later still gets read.
701
+ */
702
+ function flattenProperties(node, checker, seen) {
703
+ const properties = new Map();
704
+ let unreadable = false;
705
+ if (seen.has(node)) {
706
+ return { properties, unreadable };
707
+ }
708
+ seen.add(node);
709
+ for (const property of node.properties) {
710
+ if (ts.isSpreadAssignment(property)) {
711
+ const spread = resolveObjectExpression(property.expression, checker, new Set());
712
+ if (!spread) {
713
+ unreadable = true;
714
+ continue;
715
+ }
716
+ const nested = flattenProperties(spread, checker, seen);
717
+ unreadable ||= nested.unreadable;
718
+ for (const [key, value] of nested.properties) {
719
+ properties.set(key, value);
720
+ }
721
+ continue;
722
+ }
723
+ const key = property.name ? getPropertyName(property.name) : null;
724
+ if (!key) {
725
+ // A computed key could be either of the ones we look for.
726
+ unreadable = true;
727
+ continue;
728
+ }
729
+ properties.set(key, ts.isPropertyAssignment(property) ? property.initializer : null);
730
+ }
731
+ seen.delete(node);
732
+ return { properties, unreadable };
733
+ }
734
+ function readParserOptions(node, checker) {
735
+ const { properties, unreadable } = flattenProperties(node, checker, new Set());
736
+ const project = properties.get('project');
737
+ return {
738
+ projectService: !properties.has('projectService')
739
+ ? 'absent'
740
+ : isFalsyLiteral(properties.get('projectService'))
741
+ ? 'disabled'
742
+ : 'enabled',
743
+ // A shorthand (`{ project }`) hides its value, and an unreadable spread could
744
+ // carry any `project` at all; both count, since assuming typed linting is off
745
+ // risks appending a conflicting block over a working config.
746
+ enablesProject: unreadable || (properties.has('project') && !isFalsyLiteral(project)),
747
+ };
748
+ }
749
+ function getPropertyName(name) {
750
+ return ts.isIdentifier(name) ||
751
+ ts.isStringLiteral(name) ||
752
+ ts.isNumericLiteral(name)
753
+ ? name.text
754
+ : null;
755
+ }
756
+ function isFalsyLiteral(node) {
757
+ if (!node) {
758
+ return false;
759
+ }
760
+ return (node.kind === ts.SyntaxKind.FalseKeyword ||
761
+ node.kind === ts.SyntaxKind.NullKeyword ||
762
+ (ts.isIdentifier(node) && node.text === 'undefined'));
763
+ }
764
+ /**
765
+ * Adds a typed-linting block (`parserOptions.projectService` + `tsconfigRootDir`)
766
+ * to a project's flat ESLint config. No-op for legacy `.eslintrc` configs, whose
767
+ * JSON format cannot express the `__dirname` that `tsconfigRootDir` needs.
768
+ *
769
+ * Use after operations that strip existing overrides (e.g.
770
+ * `replaceOverridesInLintConfig`) to re-establish typed linting.
771
+ */
772
+ function addTypedLintingToFlatConfig(tree, root) {
773
+ if (!(0, flat_config_1.useFlatConfig)(tree)) {
774
+ return;
775
+ }
776
+ let fileName;
777
+ for (const f of flat_config_1.eslintFlatConfigFilenames) {
778
+ if (tree.exists((0, devkit_1.joinPathFragments)(root, f))) {
779
+ fileName = (0, devkit_1.joinPathFragments)(root, f);
780
+ break;
781
+ }
782
+ }
783
+ if (!fileName) {
784
+ return;
785
+ }
786
+ const content = tree.read(fileName, 'utf8');
787
+ // Idempotent: leave the config alone only when it configures typed-linting
788
+ // parser options itself, an explicit `projectService: false` opt-out included.
789
+ // Typed linting a config merely spreads in from another file need not cover
790
+ // this project (its globs may be scoped elsewhere), and the appended block
791
+ // defuses any inherited `project` anyway, so it is no reason to skip.
792
+ const report = inspectTypedLinting(content);
793
+ if (report.own) {
794
+ return;
795
+ }
796
+ if (report.uncertain) {
797
+ // A local `parserOptions` set through an expression we can't read could
798
+ // already enable `project`; appending would silently convert it to the
799
+ // project service, so leave the config for the user to complete instead.
800
+ devkit_1.logger.warn(`Could not tell whether typed linting is already set up in "${fileName}" because its \`parserOptions\` is built from an expression Nx cannot read statically. Left it unchanged; add \`languageOptions: { parserOptions: { projectService: true } }\` yourself if typed linting is not configured.`);
801
+ return;
802
+ }
803
+ // The block carries `tsconfigRootDir`, whose value differs per module system,
804
+ // so the extension has to win over the content where it is decisive.
805
+ const format = determineEslintConfigFormatForFile(fileName, content);
806
+ const block = (0, ast_utils_1.generateTypedLintingFlatConfigOverride)(format);
807
+ const updated = (0, ast_utils_1.addBlockToFlatConfigExport)(content, block);
808
+ if (updated === content) {
809
+ // `addBlockToFlatConfigExport` only edits a plain array export
810
+ // (`export default [...]` / `module.exports = [...]`). A wrapper config such
811
+ // as `export default tseslint.config(...)` is left untouched, so warn rather
812
+ // than silently dropping the request.
813
+ devkit_1.logger.warn(`Could not enable typed linting in "${fileName}" because its ESLint flat config is not a plain array export. Add \`languageOptions: { parserOptions: { projectService: true } }\` to enable typed linting.`);
814
+ return;
815
+ }
816
+ tree.write(fileName, updated);
136
817
  }
137
818
  function addOverrideToLintConfig(tree, root, override, options = {
138
819
  insertAtTheEnd: true,
@@ -157,7 +838,7 @@ function addOverrideToLintConfig(tree, root, override, options = {
157
838
  }
158
839
  }
159
840
  let content = tree.read(fileName, 'utf8');
160
- const format = content.includes('export default') ? 'mjs' : 'cjs';
841
+ const format = determineEslintConfigFormatForFile(fileName, content);
161
842
  const flatOverride = (0, ast_utils_1.generateFlatOverride)(override, format);
162
843
  // Check if the provided override using legacy eslintrc properties or plugins, if so we need to add compat
163
844
  if ((0, ast_utils_1.overrideNeedsCompat)(override)) {
@@ -271,7 +952,7 @@ function replaceOverridesInLintConfig(tree, root, overrides) {
271
952
  }
272
953
  }
273
954
  let content = tree.read(fileName, 'utf8');
274
- const format = content.includes('export default') ? 'mjs' : 'cjs';
955
+ const format = determineEslintConfigFormatForFile(fileName, content);
275
956
  // Check if any of the provided overrides using legacy eslintrc properties or plugins, if so we need to add compat
276
957
  if (overrides.some(ast_utils_1.overrideNeedsCompat)) {
277
958
  content = (0, ast_utils_1.addFlatCompatToFlatConfig)(content);
@@ -25,6 +25,11 @@ export declare function addImportToFlatConfig(content: string, variable: string
25
25
  * Remove an import from flat config
26
26
  */
27
27
  export declare function removeImportFromFlatConfig(content: string, variable: string, imp: string): string;
28
+ /**
29
+ * Whether a flat config uses an ESM `export default` (vs CJS `module.exports`).
30
+ * AST-based so a commented-out `export default` in a CJS file isn't misdetected.
31
+ */
32
+ export declare function isEsmExport(source: ts.SourceFile): boolean;
28
33
  /**
29
34
  * Injects new ts.expression to the end of the module.exports or export default array.
30
35
  */
@@ -75,6 +80,12 @@ export declare function generateFlatOverride(_override: Partial<Linter.ConfigOve
75
80
  ignores?: Linter.FlatConfig['ignores'];
76
81
  }, format: 'mjs' | 'cjs', importsMap?: Map<string, string>): ts.ObjectLiteralExpression | ts.SpreadElement;
77
82
  export declare function generateFlatPredefinedConfig(predefinedConfigName: string, moduleName?: string, spread?: boolean): ts.ObjectLiteralExpression | ts.SpreadElement | ts.ElementAccessExpression;
83
+ /**
84
+ * Generates a flat-config override block enabling typed linting via the
85
+ * project service. Default files match the typed-linting block that
86
+ * generators historically emitted via `parserOptions.project`.
87
+ */
88
+ export declare function generateTypedLintingFlatConfigOverride(format: 'mjs' | 'cjs', files?: string[]): ts.ObjectLiteralExpression;
78
89
  export declare function mapFilePaths<T extends Partial<Linter.ConfigOverride<Linter.RulesRecord>>>(_override: T): T;
79
90
  /**
80
91
  * Generates an AST from a JSON-type input
@@ -7,6 +7,7 @@ exports.hasOverride = hasOverride;
7
7
  exports.replaceOverride = replaceOverride;
8
8
  exports.addImportToFlatConfig = addImportToFlatConfig;
9
9
  exports.removeImportFromFlatConfig = removeImportFromFlatConfig;
10
+ exports.isEsmExport = isEsmExport;
10
11
  exports.addBlockToFlatConfigExport = addBlockToFlatConfigExport;
11
12
  exports.removePlugin = removePlugin;
12
13
  exports.removeCompatExtends = removeCompatExtends;
@@ -23,6 +24,7 @@ exports.generateESMImport = generateESMImport;
23
24
  exports.overrideNeedsCompat = overrideNeedsCompat;
24
25
  exports.generateFlatOverride = generateFlatOverride;
25
26
  exports.generateFlatPredefinedConfig = generateFlatPredefinedConfig;
27
+ exports.generateTypedLintingFlatConfigOverride = generateTypedLintingFlatConfigOverride;
26
28
  exports.mapFilePaths = mapFilePaths;
27
29
  exports.generateAst = generateAst;
28
30
  const tslib_1 = require("tslib");
@@ -39,7 +41,7 @@ const SPREAD_ELEMENTS_REGEXP = /\s*\.\.\.[a-zA-Z0-9_]+(\.[a-zA-Z0-9_]+)*,?\n?/g;
39
41
  */
40
42
  function removeOverridesFromLintConfig(content) {
41
43
  const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
42
- const format = content.includes('export default') ? 'mjs' : 'cjs';
44
+ const format = isEsmExport(source) ? 'mjs' : 'cjs';
43
45
  const exportsArray = format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
44
46
  if (!exportsArray) {
45
47
  return content;
@@ -78,7 +80,7 @@ function findModuleExports(source) {
78
80
  }
79
81
  function addPatternsToFlatConfigIgnoresBlock(content, ignorePatterns) {
80
82
  const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
81
- const format = content.includes('export default') ? 'mjs' : 'cjs';
83
+ const format = isEsmExport(source) ? 'mjs' : 'cjs';
82
84
  const exportsArray = format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
83
85
  if (!exportsArray) {
84
86
  return content;
@@ -110,7 +112,7 @@ function addPatternsToFlatConfigIgnoresBlock(content, ignorePatterns) {
110
112
  }
111
113
  function hasFlatConfigIgnoresBlock(content) {
112
114
  const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
113
- const format = content.includes('export default') ? 'mjs' : 'cjs';
115
+ const format = isEsmExport(source) ? 'mjs' : 'cjs';
114
116
  const exportsArray = format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
115
117
  if (!exportsArray) {
116
118
  return false;
@@ -137,7 +139,7 @@ function isOverride(node) {
137
139
  }
138
140
  function hasOverride(content, lookup) {
139
141
  const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
140
- const format = content.includes('export default') ? 'mjs' : 'cjs';
142
+ const format = isEsmExport(source) ? 'mjs' : 'cjs';
141
143
  const exportsArray = format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
142
144
  if (!exportsArray) {
143
145
  return false;
@@ -295,7 +297,7 @@ function serializeValue(value, format) {
295
297
  */
296
298
  function replaceOverride(content, root, lookup, update) {
297
299
  const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
298
- const format = content.includes('export default') ? 'mjs' : 'cjs';
300
+ const format = isEsmExport(source) ? 'mjs' : 'cjs';
299
301
  const exportsArray = format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
300
302
  if (!exportsArray) {
301
303
  return content;
@@ -391,7 +393,7 @@ function replaceOverride(content, root, lookup, update) {
391
393
  function addImportToFlatConfig(content, variable, imp) {
392
394
  const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
393
395
  const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
394
- const format = content.includes('export default') ? 'mjs' : 'cjs';
396
+ const format = isEsmExport(source) ? 'mjs' : 'cjs';
395
397
  if (format === 'mjs') {
396
398
  return addESMImportToFlatConfig(source, printer, content, variable, imp);
397
399
  }
@@ -547,7 +549,7 @@ function existsAsNamedOrDefaultImport(node, variable) {
547
549
  */
548
550
  function removeImportFromFlatConfig(content, variable, imp) {
549
551
  const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
550
- const format = content.includes('export default') ? 'mjs' : 'cjs';
552
+ const format = isEsmExport(source) ? 'mjs' : 'cjs';
551
553
  if (format === 'mjs') {
552
554
  return removeImportFromFlatConfigESM(source, content, variable, imp);
553
555
  }
@@ -602,6 +604,13 @@ function removeImportFromFlatConfigCJS(source, content, variable, imp) {
602
604
  });
603
605
  return (0, devkit_1.applyChangesToString)(content, changes);
604
606
  }
607
+ /**
608
+ * Whether a flat config uses an ESM `export default` (vs CJS `module.exports`).
609
+ * AST-based so a commented-out `export default` in a CJS file isn't misdetected.
610
+ */
611
+ function isEsmExport(source) {
612
+ return source.statements.some((statement) => ts.isExportAssignment(statement) && !statement.isExportEquals);
613
+ }
605
614
  /**
606
615
  * Injects new ts.expression to the end of the module.exports or export default array.
607
616
  */
@@ -610,7 +619,9 @@ function addBlockToFlatConfigExport(content, config, options = {
610
619
  }) {
611
620
  const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
612
621
  const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
613
- const format = content.includes('export default') ? 'mjs' : 'cjs';
622
+ // AST-based format detection so `module.exports = [...]` files with a
623
+ // commented-out `export default` example aren't mis-routed to the ESM path.
624
+ const format = isEsmExport(source) ? 'mjs' : 'cjs';
614
625
  // find the export default array statement
615
626
  if (format === 'mjs') {
616
627
  return addBlockToFlatConfigExportESM(content, config, source, printer, options);
@@ -721,7 +732,7 @@ function addBlockToFlatConfigExportCJS(content, config, source, printer, options
721
732
  }
722
733
  function removePlugin(content, pluginName, pluginImport) {
723
734
  const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
724
- const format = content.includes('export default') ? 'mjs' : 'cjs';
735
+ const format = isEsmExport(source) ? 'mjs' : 'cjs';
725
736
  const changes = [];
726
737
  if (format === 'mjs') {
727
738
  ts.forEachChild(source, function analyze(node) {
@@ -851,7 +862,7 @@ function removePlugin(content, pluginName, pluginImport) {
851
862
  function removeCompatExtends(content, compatExtends) {
852
863
  const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
853
864
  const changes = [];
854
- const format = content.includes('export default') ? 'mjs' : 'cjs';
865
+ const format = isEsmExport(source) ? 'mjs' : 'cjs';
855
866
  const exportsArray = format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
856
867
  if (!exportsArray) {
857
868
  return content;
@@ -890,7 +901,7 @@ function removeCompatExtends(content, compatExtends) {
890
901
  }
891
902
  function removePredefinedConfigs(content, moduleImport, moduleVariable, configs) {
892
903
  const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
893
- const format = content.includes('export default') ? 'mjs' : 'cjs';
904
+ const format = isEsmExport(source) ? 'mjs' : 'cjs';
894
905
  const changes = [];
895
906
  let removeImport = true;
896
907
  const exportsArray = format === 'mjs' ? findExportDefault(source) : findModuleExports(source);
@@ -943,7 +954,8 @@ function addPluginsToExportsBlock(content, plugins) {
943
954
  */
944
955
  function addFlatCompatToFlatConfig(content) {
945
956
  const result = addImportToFlatConfig(content, 'js', '@eslint/js');
946
- const format = content.includes('export default') ? 'mjs' : 'cjs';
957
+ const source = ts.createSourceFile('', content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
958
+ const format = isEsmExport(source) ? 'mjs' : 'cjs';
947
959
  if (result.includes('const compat = new FlatCompat')) {
948
960
  return result;
949
961
  }
@@ -1172,8 +1184,17 @@ function generateFlatOverride(_override, format, importsMap) {
1172
1184
  },
1173
1185
  });
1174
1186
  }
1175
- // At this point we are applying the flat config compat tooling to the override
1176
- let { excludedFiles, parser, parserOptions, rules, files, ...rest } = override;
1187
+ // At this point we are applying the flat config compat tooling to the override.
1188
+ // Only destructure what Nx must remap or merge itself. Everything else, including
1189
+ // parser and parserOptions, stays in `rest` so FlatCompat translates it: it resolves
1190
+ // the parser to a module reference and hoists ecmaVersion/sourceType out of
1191
+ // parserOptions, which re-emitting them by hand does not do.
1192
+ let { excludedFiles, rules, files, ...rest } = override;
1193
+ // eslintrc accepts `parser: null` to mean "use the default parser". Flat config
1194
+ // has no null-parser concept and FlatCompat throws on it, so omit the key.
1195
+ if (rest.parser === null) {
1196
+ delete rest.parser;
1197
+ }
1177
1198
  const objectLiteralElements = [
1178
1199
  ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config')),
1179
1200
  ];
@@ -1211,11 +1232,6 @@ function generateFlatOverride(_override, format, importsMap) {
1211
1232
  ...rulesObjectInitializer.properties,
1212
1233
  ];
1213
1234
  objectLiteralElements.push(ts.factory.createPropertyAssignment('rules', ts.factory.createObjectLiteralExpression(updatedRulesProperties, true)));
1214
- if (parserOptions) {
1215
- addTSObjectProperty(objectLiteralElements, 'languageOptions', {
1216
- parserOptions,
1217
- });
1218
- }
1219
1235
  return ts.factory.createSpreadElement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier('compat'), ts.factory.createIdentifier('config')), undefined, [generateAst(rest)]), ts.factory.createIdentifier('map')), undefined, [
1220
1236
  ts.factory.createArrowFunction(undefined, undefined, [
1221
1237
  ts.factory.createParameterDeclaration(undefined, undefined, 'config'),
@@ -1247,6 +1263,56 @@ function generateFlatPredefinedConfig(predefinedConfigName, moduleName = 'nx', s
1247
1263
  const node = ts.factory.createElementAccessExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(moduleName), ts.factory.createIdentifier('configs')), ts.factory.createStringLiteral(predefinedConfigName));
1248
1264
  return spread ? ts.factory.createSpreadElement(node) : node;
1249
1265
  }
1266
+ const DEFAULT_TYPED_LINTING_FILES = [
1267
+ '**/*.ts',
1268
+ '**/*.tsx',
1269
+ '**/*.js',
1270
+ '**/*.jsx',
1271
+ ];
1272
+ /**
1273
+ * Generates the AST for a `parserOptions` object that enables typed linting
1274
+ * via typescript-eslint's project service (the recommended approach since
1275
+ * typescript-eslint v8).
1276
+ *
1277
+ * Emits `tsconfigRootDir: import.meta.dirname` for `mjs` and
1278
+ * `tsconfigRootDir: __dirname` for `cjs`.
1279
+ */
1280
+ function generateProjectServiceParserOptions(format) {
1281
+ return ts.factory.createObjectLiteralExpression([
1282
+ ts.factory.createPropertyAssignment('projectService', ts.factory.createTrue()),
1283
+ generateProjectDefusingAssignment(),
1284
+ ts.factory.createPropertyAssignment('tsconfigRootDir', format === 'mjs'
1285
+ ? ts.factory.createPropertyAccessExpression(ts.factory.createMetaProperty(ts.SyntaxKind.ImportKeyword, ts.factory.createIdentifier('meta')), ts.factory.createIdentifier('dirname'))
1286
+ : ts.factory.createIdentifier('__dirname')),
1287
+ ], true);
1288
+ }
1289
+ /**
1290
+ * ESLint merges `parserOptions` across every config entry matching a file, and
1291
+ * typescript-eslint rejects a merged truthy `project` next to `projectService`.
1292
+ * A config can pick one up from anywhere it extends, spreads in, or composes,
1293
+ * and several of those routes cannot be read statically, so the block
1294
+ * neutralizes it outright. The comment carries the condition for dropping it,
1295
+ * since only the user can check what the configs they pull in set.
1296
+ */
1297
+ function generateProjectDefusingAssignment() {
1298
+ const assignment = ts.factory.createPropertyAssignment('project', ts.factory.createNull());
1299
+ ts.addSyntheticLeadingComment(assignment, ts.SyntaxKind.SingleLineCommentTrivia, ' `projectService` conflicts with a `parserOptions.project` set by any config', true);
1300
+ ts.addSyntheticLeadingComment(assignment, ts.SyntaxKind.SingleLineCommentTrivia, ' merged into this one. Remove this once you know none of them set it.', true);
1301
+ return assignment;
1302
+ }
1303
+ /**
1304
+ * Generates a flat-config override block enabling typed linting via the
1305
+ * project service. Default files match the typed-linting block that
1306
+ * generators historically emitted via `parserOptions.project`.
1307
+ */
1308
+ function generateTypedLintingFlatConfigOverride(format, files = DEFAULT_TYPED_LINTING_FILES) {
1309
+ return ts.factory.createObjectLiteralExpression([
1310
+ ts.factory.createPropertyAssignment('files', ts.factory.createArrayLiteralExpression(files.map((f) => ts.factory.createStringLiteral(f)), true)),
1311
+ ts.factory.createPropertyAssignment('languageOptions', ts.factory.createObjectLiteralExpression([
1312
+ ts.factory.createPropertyAssignment('parserOptions', generateProjectServiceParserOptions(format)),
1313
+ ], true)),
1314
+ ], true);
1315
+ }
1250
1316
  function mapFilePaths(_override) {
1251
1317
  const override = {
1252
1318
  ..._override,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nx/eslint",
3
- "version": "23.1.0",
3
+ "version": "23.1.1",
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"
76
+ "@nx/jest": "23.1.1"
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",
83
- "@nx/js": "23.1.0"
82
+ "@nx/js": "23.1.1",
83
+ "@nx/devkit": "23.1.1"
84
84
  },
85
85
  "devDependencies": {
86
- "nx": "23.1.0"
86
+ "nx": "23.1.1"
87
87
  },
88
88
  "peerDependenciesMeta": {
89
89
  "@nx/jest": {