@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.
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +4 -1
- package/dist/src/generators/lint-project/lint-project.d.ts +4 -0
- package/dist/src/generators/lint-project/lint-project.js +13 -18
- package/dist/src/generators/utils/eslint-file.d.ts +55 -0
- package/dist/src/generators/utils/eslint-file.js +686 -5
- package/dist/src/generators/utils/flat-config/ast-utils.d.ts +11 -0
- package/dist/src/generators/utils/flat-config/ast-utils.js +85 -19
- package/package.json +5 -5
package/dist/internal.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
?
|
|
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: !
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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/
|
|
83
|
-
"@nx/
|
|
82
|
+
"@nx/js": "23.1.1",
|
|
83
|
+
"@nx/devkit": "23.1.1"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
|
-
"nx": "23.1.
|
|
86
|
+
"nx": "23.1.1"
|
|
87
87
|
},
|
|
88
88
|
"peerDependenciesMeta": {
|
|
89
89
|
"@nx/jest": {
|