@nx/eslint 0.0.0-pr-22179-271588f → 0.0.0-pr-26515-856ef7f

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.
Files changed (35) hide show
  1. package/LICENSE +1 -1
  2. package/generators.json +5 -0
  3. package/migrations.json +17 -12
  4. package/package.json +12 -10
  5. package/plugin.d.ts +1 -1
  6. package/plugin.js +2 -1
  7. package/src/executors/lint/lint.impl.d.ts +1 -1
  8. package/src/executors/lint/lint.impl.js +23 -18
  9. package/src/executors/lint/utility/eslint-utils.js +4 -17
  10. package/src/generators/convert-to-flat-config/generator.d.ts +2 -2
  11. package/src/generators/convert-to-flat-config/generator.js +8 -2
  12. package/src/generators/convert-to-inferred/convert-to-inferred.d.ts +7 -0
  13. package/src/generators/convert-to-inferred/convert-to-inferred.js +80 -0
  14. package/src/generators/convert-to-inferred/lib/target-options-map.d.ts +16 -0
  15. package/src/generators/convert-to-inferred/lib/target-options-map.js +19 -0
  16. package/src/generators/convert-to-inferred/schema.json +19 -0
  17. package/src/generators/init/init-migration.js +4 -0
  18. package/src/generators/init/init.d.ts +1 -1
  19. package/src/generators/init/init.js +16 -27
  20. package/src/generators/lint-project/lint-project.d.ts +2 -1
  21. package/src/generators/lint-project/lint-project.js +16 -20
  22. package/src/generators/utils/eslint-file.js +4 -2
  23. package/src/generators/utils/flat-config/ast-utils.js +31 -7
  24. package/src/plugins/plugin.d.ts +3 -1
  25. package/src/plugins/plugin.js +162 -55
  26. package/src/utils/config-file.d.ts +4 -1
  27. package/src/utils/config-file.js +50 -16
  28. package/src/utils/resolve-eslint-class.d.ts +2 -0
  29. package/src/utils/resolve-eslint-class.js +23 -0
  30. package/src/utils/versions.d.ts +2 -2
  31. package/src/utils/versions.js +2 -2
  32. package/src/migrations/update-15-0-0/add-eslint-inputs.d.ts +0 -2
  33. package/src/migrations/update-15-0-0/add-eslint-inputs.js +0 -27
  34. package/src/migrations/update-15-7-1/add-eslint-ignore.d.ts +0 -2
  35. package/src/migrations/update-15-7-1/add-eslint-ignore.js +0 -36
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  (The MIT License)
2
2
 
3
- Copyright (c) 2017-2023 Narwhal Technologies Inc.
3
+ Copyright (c) 2017-2024 Narwhal Technologies Inc.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining
6
6
  a copy of this software and associated documentation files (the
package/generators.json CHANGED
@@ -23,6 +23,11 @@
23
23
  "factory": "./src/generators/convert-to-flat-config/generator",
24
24
  "schema": "./src/generators/convert-to-flat-config/schema.json",
25
25
  "description": "Convert an Nx workspace's ESLint configs to use Flat Config."
26
+ },
27
+ "convert-to-inferred": {
28
+ "factory": "./src/generators/convert-to-inferred/convert-to-inferred",
29
+ "schema": "./src/generators/convert-to-inferred/schema.json",
30
+ "description": "Convert existing ESLint project(s) using `@nx/eslint:lint` executor to use `@nx/eslint/plugin`."
26
31
  }
27
32
  }
28
33
  }
package/migrations.json CHANGED
@@ -1,17 +1,5 @@
1
1
  {
2
2
  "generators": {
3
- "add-eslint-inputs": {
4
- "cli": "nx",
5
- "version": "15.0.0-beta.0",
6
- "description": "Stop hashing eslint config files for build targets and dependent tasks",
7
- "factory": "./src/migrations/update-15-0-0/add-eslint-inputs"
8
- },
9
- "add-eslint-ignore": {
10
- "cli": "nx",
11
- "version": "15.7.1-beta.0",
12
- "description": "Add node_modules to root eslint ignore",
13
- "factory": "./src/migrations/update-15-7-1/add-eslint-ignore"
14
- },
15
3
  "update-16-0-0-add-nx-packages": {
16
4
  "cli": "nx",
17
5
  "version": "16.0.0-beta.1",
@@ -133,6 +121,23 @@
133
121
  "version": "^6.13.2"
134
122
  }
135
123
  }
124
+ },
125
+ "18.2.0": {
126
+ "version": "18.2.0-beta.0",
127
+ "packages": {
128
+ "@typescript-eslint/parser": {
129
+ "version": "^7.3.0"
130
+ },
131
+ "@typescript-eslint/eslint-plugin": {
132
+ "version": "^7.3.0"
133
+ },
134
+ "@typescript-eslint/utils": {
135
+ "version": "^7.3.0"
136
+ },
137
+ "eslint": {
138
+ "version": "~8.57.0"
139
+ }
140
+ }
136
141
  }
137
142
  }
138
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nx/eslint",
3
- "version": "0.0.0-pr-22179-271588f",
3
+ "version": "0.0.0-pr-26515-856ef7f",
4
4
  "private": false,
5
5
  "description": "The ESLint plugin for Nx contains executors, generators and utilities used for linting JavaScript/TypeScript projects within an Nx workspace.",
6
6
  "repository": {
@@ -13,9 +13,10 @@
13
13
  "Web",
14
14
  "Lint",
15
15
  "ESLint",
16
- "CLI"
16
+ "CLI",
17
+ "Testing"
17
18
  ],
18
- "main": "./index.js",
19
+ "main": "./index",
19
20
  "typings": "./index.d.ts",
20
21
  "author": "Victor Savkin",
21
22
  "license": "MIT",
@@ -30,18 +31,19 @@
30
31
  "generators": "./generators.json",
31
32
  "executors": "./executors.json",
32
33
  "peerDependencies": {
33
- "js-yaml": "4.1.0"
34
+ "@zkochan/js-yaml": "0.0.7",
35
+ "eslint": "^8.0.0 || ^9.0.0"
34
36
  },
35
37
  "dependencies": {
36
- "@nx/devkit": "0.0.0-pr-22179-271588f",
37
- "@nx/js": "0.0.0-pr-22179-271588f",
38
- "eslint": "^8.0.0",
38
+ "@nx/devkit": "0.0.0-pr-26515-856ef7f",
39
+ "@nx/js": "0.0.0-pr-26515-856ef7f",
40
+ "semver": "^7.5.3",
39
41
  "tslib": "^2.3.0",
40
- "typescript": "~5.3.2",
41
- "@nx/linter": "0.0.0-pr-22179-271588f"
42
+ "typescript": "~5.4.2",
43
+ "@nx/linter": "0.0.0-pr-26515-856ef7f"
42
44
  },
43
45
  "peerDependenciesMeta": {
44
- "js-yaml": {
46
+ "@zkochan/js-yaml": {
45
47
  "optional": true
46
48
  }
47
49
  },
package/plugin.d.ts CHANGED
@@ -1 +1 @@
1
- export { createNodes, EslintPluginOptions } from './src/plugins/plugin';
1
+ export { createNodes, createNodesV2, EslintPluginOptions, } from './src/plugins/plugin';
package/plugin.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createNodes = void 0;
3
+ exports.createNodesV2 = exports.createNodes = void 0;
4
4
  var plugin_1 = require("./src/plugins/plugin");
5
5
  Object.defineProperty(exports, "createNodes", { enumerable: true, get: function () { return plugin_1.createNodes; } });
6
+ Object.defineProperty(exports, "createNodesV2", { enumerable: true, get: function () { return plugin_1.createNodesV2; } });
@@ -1,4 +1,4 @@
1
- import { ExecutorContext } from '@nx/devkit';
1
+ import { type ExecutorContext } from '@nx/devkit';
2
2
  import type { Schema } from './schema';
3
3
  export default function run(options: Schema, context: ExecutorContext): Promise<{
4
4
  success: boolean;
@@ -2,9 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const devkit_1 = require("@nx/devkit");
4
4
  const fs_1 = require("fs");
5
+ const utils_1 = require("nx/src/tasks-runner/utils");
5
6
  const path_1 = require("path");
7
+ const config_file_1 = require("../../utils/config-file");
6
8
  const eslint_utils_1 = require("./utility/eslint-utils");
7
- const utils_1 = require("nx/src/tasks-runner/utils");
8
9
  async function run(options, context) {
9
10
  // this is only used for the hasher
10
11
  delete options.hasTypeAwareRules;
@@ -23,19 +24,14 @@ async function run(options, context) {
23
24
  ? (0, devkit_1.joinPathFragments)(options.cacheLocation, projectName)
24
25
  : undefined;
25
26
  const { printConfig, errorOnUnmatchedPattern, ...normalizedOptions } = options;
26
- /**
27
- * Until ESLint v9 is released and the new so called flat config is the default
28
- * we only want to support it if the user has explicitly opted into it by converting
29
- * their root ESLint config to use eslint.config.js
30
- */
31
- const hasFlatConfig = (0, fs_1.existsSync)((0, devkit_1.joinPathFragments)(devkit_1.workspaceRoot, 'eslint.config.js'));
27
+ // locate the flat config file if it exists starting from the project root
28
+ const flatConfigFilePath = (0, config_file_1.findFlatConfigFile)(projectRoot, context.root);
29
+ const hasFlatConfig = flatConfigFilePath !== null;
32
30
  // while standard eslint uses by default closest config to the file, if otherwise not specified,
33
- // the flat config would always use the root config, so we need to explicitly set it to the local one
31
+ // the flat config would be resolved starting from the cwd, which we changed to the workspace root
32
+ // so we explicitly set the config path to the flat config file path we previously found
34
33
  if (hasFlatConfig && !normalizedOptions.eslintConfig) {
35
- const eslintConfigPath = (0, devkit_1.joinPathFragments)(projectRoot, 'eslint.config.js');
36
- if ((0, fs_1.existsSync)(eslintConfigPath)) {
37
- normalizedOptions.eslintConfig = eslintConfigPath;
38
- }
34
+ normalizedOptions.eslintConfig = path_1.posix.relative(systemRoot, flatConfigFilePath);
39
35
  }
40
36
  /**
41
37
  * We want users to have the option of not specifying the config path, and let
@@ -81,15 +77,24 @@ async function run(options, context) {
81
77
  catch (err) {
82
78
  if (err.message.includes('You must therefore provide a value for the "parserOptions.project" property for @typescript-eslint/parser')) {
83
79
  const ruleName = err.message.match(/rule '([^']+)':/)?.[1];
84
- let eslintConfigPathForError = `for ${projectName}`;
85
- if (context.projectsConfigurations?.projects?.[projectName]?.root) {
86
- const { root } = context.projectsConfigurations.projects[projectName];
87
- eslintConfigPathForError = `\`${root}/.eslintrc.json\``;
80
+ const reportedFile = err.message.match(/Occurred while linting (.+)$/)?.[1];
81
+ let eslintConfigPathForError = `for the project "${projectName}"`;
82
+ if (eslintConfigPath) {
83
+ eslintConfigPathForError = `"${path_1.posix.relative(context.root, eslintConfigPath)}"`;
84
+ }
85
+ else {
86
+ const configPathForfile = hasFlatConfig
87
+ ? (0, config_file_1.findFlatConfigFile)(projectRoot, context.root)
88
+ : (0, config_file_1.findOldConfigFile)(reportedFile ?? projectRoot, context.root);
89
+ if (configPathForfile) {
90
+ eslintConfigPathForError = `"${path_1.posix.relative(context.root, configPathForfile)}"`;
91
+ }
88
92
  }
89
93
  console.error(`
90
- Error: You have attempted to use ${ruleName ? `the lint rule ${ruleName}` : 'a lint rule'} which requires the full TypeScript type-checker to be available, but you do not have \`parserOptions.project\` configured to point at your project tsconfig.json files in the relevant TypeScript file "overrides" block of your project ESLint config ${eslintConfigPath || eslintConfigPathForError}
94
+ Error: You have attempted to use ${ruleName ? `the lint rule "${ruleName}"` : 'a lint rule'} which requires the full TypeScript type-checker to be available, but you do not have "parserOptions.project" configured to point at your project tsconfig.json files in the relevant TypeScript file "overrides" block of your ESLint config ${eslintConfigPathForError}
95
+ ${reportedFile ? `Occurred while linting ${reportedFile}` : ''}
91
96
 
92
- Please see https://nx.dev/guides/eslint for full guidance on how to resolve this issue.
97
+ Please see https://nx.dev/recipes/tips-n-tricks/eslint for full guidance on how to resolve this issue.
93
98
  `);
94
99
  return {
95
100
  success: false,
@@ -1,26 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveAndInstantiateESLint = void 0;
4
- async function resolveESLintClass(useFlatConfig = false) {
5
- try {
6
- if (!useFlatConfig) {
7
- return (await Promise.resolve().then(() => require('eslint'))).ESLint;
8
- }
9
- // eslint-disable-next-line @typescript-eslint/no-var-requires
10
- const { FlatESLint } = require('eslint/use-at-your-own-risk');
11
- return FlatESLint;
12
- }
13
- catch {
14
- throw new Error('Unable to find ESLint. Ensure ESLint is installed.');
15
- }
16
- }
4
+ const config_file_1 = require("../../../utils/config-file");
5
+ const resolve_eslint_class_1 = require("../../../utils/resolve-eslint-class");
17
6
  async function resolveAndInstantiateESLint(eslintConfigPath, options, useFlatConfig = false) {
18
- if (useFlatConfig &&
19
- eslintConfigPath &&
20
- !eslintConfigPath?.endsWith('eslint.config.js')) {
7
+ if (useFlatConfig && eslintConfigPath && !(0, config_file_1.isFlatConfig)(eslintConfigPath)) {
21
8
  throw new Error('When using the new Flat Config with ESLint, all configs must be named eslint.config.js and .eslintrc files may not be used. See https://eslint.org/docs/latest/use/configure/configuration-files-new');
22
9
  }
23
- const ESLint = await resolveESLintClass(useFlatConfig);
10
+ const ESLint = await (0, resolve_eslint_class_1.resolveESLintClass)(useFlatConfig);
24
11
  const eslintOptions = {
25
12
  overrideConfigFile: eslintConfigPath,
26
13
  fix: !!options.fix,
@@ -1,4 +1,4 @@
1
- import { Tree } from '@nx/devkit';
1
+ import { GeneratorCallback, Tree } from '@nx/devkit';
2
2
  import { ConvertToFlatConfigGeneratorSchema } from './schema';
3
- export declare function convertToFlatConfigGenerator(tree: Tree, options: ConvertToFlatConfigGeneratorSchema): Promise<void>;
3
+ export declare function convertToFlatConfigGenerator(tree: Tree, options: ConvertToFlatConfigGeneratorSchema): Promise<void | GeneratorCallback>;
4
4
  export default convertToFlatConfigGenerator;
@@ -6,7 +6,7 @@ const eslint_file_1 = require("../utils/eslint-file");
6
6
  const path_1 = require("path");
7
7
  const versions_1 = require("../../utils/versions");
8
8
  const json_converter_1 = require("./converters/json-converter");
9
- const js_yaml_1 = require("js-yaml");
9
+ let shouldInstallDeps = false;
10
10
  async function convertToFlatConfigGenerator(tree, options) {
11
11
  const eslintFile = (0, eslint_file_1.findEslintFile)(tree);
12
12
  if (!eslintFile) {
@@ -33,6 +33,9 @@ async function convertToFlatConfigGenerator(tree, options) {
33
33
  if (!options.skipFormat) {
34
34
  await (0, devkit_1.formatFiles)(tree);
35
35
  }
36
+ if (shouldInstallDeps) {
37
+ return () => (0, devkit_1.installPackagesTask)(tree);
38
+ }
36
39
  }
37
40
  exports.convertToFlatConfigGenerator = convertToFlatConfigGenerator;
38
41
  exports.default = convertToFlatConfigGenerator;
@@ -112,7 +115,8 @@ function convertConfigToFlatConfig(tree, root, source, target, ignorePath) {
112
115
  }
113
116
  if (source.endsWith('.yaml') || source.endsWith('.yml')) {
114
117
  const originalContent = tree.read(`${root}/${source}`, 'utf-8');
115
- const config = (0, js_yaml_1.load)(originalContent, {
118
+ const { load } = require('@zkochan/js-yaml');
119
+ const config = load(originalContent, {
116
120
  json: true,
117
121
  filename: source,
118
122
  });
@@ -127,11 +131,13 @@ function processConvertedConfig(tree, root, source, target, { content, addESLint
127
131
  tree.write((0, path_1.join)(root, target), content);
128
132
  // add missing packages
129
133
  if (addESLintRC) {
134
+ shouldInstallDeps = true;
130
135
  (0, devkit_1.addDependenciesToPackageJson)(tree, {}, {
131
136
  '@eslint/eslintrc': versions_1.eslintrcVersion,
132
137
  });
133
138
  }
134
139
  if (addESLintJS) {
140
+ shouldInstallDeps = true;
135
141
  (0, devkit_1.addDependenciesToPackageJson)(tree, {}, {
136
142
  '@eslint/js': versions_1.eslintVersion,
137
143
  });
@@ -0,0 +1,7 @@
1
+ import { type Tree } from '@nx/devkit';
2
+ interface Schema {
3
+ project?: string;
4
+ skipFormat?: boolean;
5
+ }
6
+ export declare function convertToInferred(tree: Tree, options: Schema): Promise<void>;
7
+ export default convertToInferred;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.convertToInferred = void 0;
4
+ const devkit_1 = require("@nx/devkit");
5
+ const plugin_1 = require("../../plugins/plugin");
6
+ const executor_to_plugin_migrator_1 = require("@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator");
7
+ const target_options_map_1 = require("./lib/target-options-map");
8
+ const utils_1 = require("nx/src/tasks-runner/utils");
9
+ async function convertToInferred(tree, options) {
10
+ const projectGraph = await (0, devkit_1.createProjectGraphAsync)();
11
+ const migratedProjectsModern = await (0, executor_to_plugin_migrator_1.migrateExecutorToPlugin)(tree, projectGraph, '@nx/eslint:lint', '@nx/eslint/plugin', (targetName) => ({ targetName }), postTargetTransformer, plugin_1.createNodesV2, options.project);
12
+ const migratedProjectsLegacy = await (0, executor_to_plugin_migrator_1.migrateExecutorToPlugin)(tree, projectGraph, '@nrwl/linter:eslint', '@nx/eslint/plugin', (targetName) => ({ targetName }), postTargetTransformer, plugin_1.createNodesV2, options.project);
13
+ const migratedProjects = migratedProjectsModern.size + migratedProjectsLegacy.size;
14
+ if (migratedProjects === 0) {
15
+ throw new Error('Could not find any targets to migrate.');
16
+ }
17
+ if (!options.skipFormat) {
18
+ await (0, devkit_1.formatFiles)(tree);
19
+ }
20
+ }
21
+ exports.convertToInferred = convertToInferred;
22
+ function postTargetTransformer(target, tree, projectDetails) {
23
+ if (target.inputs) {
24
+ const inputs = target.inputs.filter((input) => typeof input === 'string' &&
25
+ ![
26
+ 'default',
27
+ '{workspaceRoot}/.eslintrc.json',
28
+ '{workspaceRoot}/.eslintignore',
29
+ '{workspaceRoot}/eslint.config.js',
30
+ ].includes(input));
31
+ if (inputs.length === 0) {
32
+ delete target.inputs;
33
+ }
34
+ }
35
+ if (target.options) {
36
+ if ('eslintConfig' in target.options) {
37
+ delete target.options.eslintConfig;
38
+ }
39
+ if ('force' in target.options) {
40
+ delete target.options.force;
41
+ }
42
+ if ('silent' in target.options) {
43
+ delete target.options.silent;
44
+ }
45
+ if ('hasTypeAwareRules' in target.options) {
46
+ delete target.options.hasTypeAwareRules;
47
+ }
48
+ if ('errorOnUnmatchedPattern' in target.options) {
49
+ if (!target.options.errorOnUnmatchedPattern) {
50
+ target.options['no-error-on-unmatched-pattern'] = true;
51
+ }
52
+ delete target.options.errorOnUnmatchedPattern;
53
+ }
54
+ if ('outputFile' in target.options) {
55
+ target.outputs ??= [];
56
+ target.outputs.push(target.options.outputFile);
57
+ }
58
+ for (const key in target_options_map_1.targetOptionsToCliMap) {
59
+ if (target.options[key]) {
60
+ target.options[target_options_map_1.targetOptionsToCliMap[key]] = target.options[key];
61
+ delete target.options[key];
62
+ }
63
+ }
64
+ if ('lintFilePatterns' in target.options) {
65
+ const normalizedLintFilePatterns = target.options.lintFilePatterns.map((pattern) => {
66
+ return (0, utils_1.interpolate)(pattern, {
67
+ workspaceRoot: '',
68
+ projectRoot: projectDetails.root,
69
+ projectName: projectDetails.projectName,
70
+ });
71
+ });
72
+ target.options.args = normalizedLintFilePatterns.map((pattern) => pattern.startsWith(projectDetails.root)
73
+ ? pattern.replace(new RegExp(`^${projectDetails.root}/`), './')
74
+ : pattern);
75
+ delete target.options.lintFilePatterns;
76
+ }
77
+ }
78
+ return target;
79
+ }
80
+ exports.default = convertToInferred;
@@ -0,0 +1,16 @@
1
+ export declare const targetOptionsToCliMap: {
2
+ fix: string;
3
+ format: string;
4
+ cache: string;
5
+ cacheLocation: string;
6
+ cacheStrategy: string;
7
+ noEslintrc: string;
8
+ outputFile: string;
9
+ maxWarnings: string;
10
+ quiet: string;
11
+ ignorePath: string;
12
+ rulesdir: string;
13
+ resolvePluginsRelativeTo: string;
14
+ reportUnusedDisableDirectives: string;
15
+ printConfig: string;
16
+ };
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.targetOptionsToCliMap = void 0;
4
+ exports.targetOptionsToCliMap = {
5
+ fix: 'fix',
6
+ format: 'format',
7
+ cache: 'cache',
8
+ cacheLocation: 'cache-location',
9
+ cacheStrategy: 'cache-strategy',
10
+ noEslintrc: 'no-eslintrc',
11
+ outputFile: 'output-file',
12
+ maxWarnings: 'max-warnings',
13
+ quiet: 'quiet',
14
+ ignorePath: 'ignore-path',
15
+ rulesdir: 'rulesdir',
16
+ resolvePluginsRelativeTo: 'resolve-plugins-relative-to',
17
+ reportUnusedDisableDirectives: 'report-unused-disable-directives',
18
+ printConfig: 'print-config',
19
+ };
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://json-schema.org/schema",
3
+ "$id": "NxEslintConvertToInferred",
4
+ "description": "Convert existing Eslint project(s) using `@nx/eslint:lint` executor to use `@nx/eslint/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
5
+ "title": "Convert Eslint project from executor to plugin",
6
+ "type": "object",
7
+ "properties": {
8
+ "project": {
9
+ "type": "string",
10
+ "description": "The project to convert from using the `@nx/eslint:lint` executor to use `@nx/eslint/plugin`.",
11
+ "x-priority": "important"
12
+ },
13
+ "skipFormat": {
14
+ "type": "boolean",
15
+ "description": "Whether to format files at the end of the migration.",
16
+ "default": false
17
+ }
18
+ }
19
+ }
@@ -104,6 +104,10 @@ function migrateEslintFile(projectEslintPath, tree) {
104
104
  }
105
105
  // add extends
106
106
  json.extends = json.extends || [];
107
+ // ensure extends is an array
108
+ if (typeof json.extends === 'string') {
109
+ json.extends = [json.extends];
110
+ }
107
111
  const pathToRootConfig = `${(0, devkit_1.offsetFromRoot)((0, path_1.dirname)(projectEslintPath))}${baseFile}`;
108
112
  if (json.extends.indexOf(pathToRootConfig) === -1) {
109
113
  json.extends.push(pathToRootConfig);
@@ -1,4 +1,4 @@
1
- import type { GeneratorCallback, Tree } from '@nx/devkit';
1
+ import { GeneratorCallback, Tree } from '@nx/devkit';
2
2
  export interface LinterInitOptions {
3
3
  skipPackageJson?: boolean;
4
4
  keepExistingVersions?: boolean;
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.lintInitGenerator = exports.initEsLint = void 0;
4
4
  const devkit_1 = require("@nx/devkit");
5
- const update_package_scripts_1 = require("@nx/devkit/src/utils/update-package-scripts");
5
+ const add_plugin_1 = require("@nx/devkit/src/utils/add-plugin");
6
6
  const versions_1 = require("../../utils/versions");
7
7
  const eslint_file_1 = require("../utils/eslint-file");
8
8
  const plugin_1 = require("../../plugins/plugin");
@@ -31,24 +31,6 @@ function addTargetDefaults(tree) {
31
31
  ];
32
32
  (0, devkit_1.updateNxJson)(tree, nxJson);
33
33
  }
34
- function addPlugin(tree) {
35
- const nxJson = (0, devkit_1.readNxJson)(tree);
36
- nxJson.plugins ??= [];
37
- for (const plugin of nxJson.plugins) {
38
- if (typeof plugin === 'string'
39
- ? plugin === '@nx/eslint/plugin'
40
- : plugin.plugin === '@nx/eslint/plugin') {
41
- return;
42
- }
43
- }
44
- nxJson.plugins.push({
45
- plugin: '@nx/eslint/plugin',
46
- options: {
47
- targetName: 'lint',
48
- },
49
- });
50
- (0, devkit_1.updateNxJson)(tree, nxJson);
51
- }
52
34
  async function initEsLint(tree, options) {
53
35
  const nxJson = (0, devkit_1.readNxJson)(tree);
54
36
  const addPluginDefault = process.env.NX_ADD_PLUGINS !== 'false' &&
@@ -56,11 +38,19 @@ async function initEsLint(tree, options) {
56
38
  options.addPlugin ??= addPluginDefault;
57
39
  const hasPlugin = (0, plugin_2.hasEslintPlugin)(tree);
58
40
  const rootEslintFile = (0, eslint_file_1.findEslintFile)(tree);
41
+ const graph = await (0, devkit_1.createProjectGraphAsync)();
42
+ const lintTargetNames = [
43
+ 'lint',
44
+ 'eslint:lint',
45
+ 'eslint-lint',
46
+ '_lint',
47
+ '_eslint:lint',
48
+ '_eslint-lint',
49
+ ];
59
50
  if (rootEslintFile && options.addPlugin && !hasPlugin) {
60
- addPlugin(tree);
61
- if (options.updatePackageScripts) {
62
- await (0, update_package_scripts_1.updatePackageScripts)(tree, plugin_1.createNodes);
63
- }
51
+ await (0, add_plugin_1.addPlugin)(tree, graph, '@nx/eslint/plugin', plugin_1.createNodesV2, {
52
+ targetName: lintTargetNames,
53
+ }, options.updatePackageScripts);
64
54
  return () => { };
65
55
  }
66
56
  if (rootEslintFile) {
@@ -68,7 +58,9 @@ async function initEsLint(tree, options) {
68
58
  }
69
59
  updateProductionFileset(tree);
70
60
  if (options.addPlugin) {
71
- addPlugin(tree);
61
+ await (0, add_plugin_1.addPlugin)(tree, graph, '@nx/eslint/plugin', plugin_1.createNodesV2, {
62
+ targetName: lintTargetNames,
63
+ }, options.updatePackageScripts);
72
64
  }
73
65
  else {
74
66
  addTargetDefaults(tree);
@@ -81,9 +73,6 @@ async function initEsLint(tree, options) {
81
73
  eslint: versions_1.eslintVersion,
82
74
  }, undefined, options.keepExistingVersions));
83
75
  }
84
- if (options.updatePackageScripts) {
85
- await (0, update_package_scripts_1.updatePackageScripts)(tree, plugin_1.createNodes);
86
- }
87
76
  return (0, devkit_1.runTasksInSerial)(...tasks);
88
77
  }
89
78
  exports.initEsLint = initEsLint;
@@ -1,4 +1,4 @@
1
- import type { GeneratorCallback, Tree } from '@nx/devkit';
1
+ import { GeneratorCallback, Tree } from '@nx/devkit';
2
2
  import { Linter as LinterEnum } from '../utils/linter';
3
3
  interface LintProjectOptions {
4
4
  project: string;
@@ -16,6 +16,7 @@ interface LintProjectOptions {
16
16
  * @internal
17
17
  */
18
18
  addExplicitTargets?: boolean;
19
+ addPackageJsonDependencyChecks?: boolean;
19
20
  }
20
21
  export declare function lintProjectGenerator(tree: Tree, options: LintProjectOptions): Promise<GeneratorCallback>;
21
22
  export declare function lintProjectGeneratorInternal(tree: Tree, options: LintProjectOptions): Promise<GeneratorCallback>;
@@ -73,7 +73,8 @@ async function lintProjectGeneratorInternal(tree, options) {
73
73
  if (!options.rootProject) {
74
74
  const projects = {};
75
75
  (0, project_configuration_1.getProjects)(tree).forEach((v, k) => (projects[k] = v));
76
- if (isMigrationToMonorepoNeeded(projects, tree)) {
76
+ const graph = await (0, devkit_1.createProjectGraphAsync)();
77
+ if (isMigrationToMonorepoNeeded(tree, graph)) {
77
78
  // we only migrate project configurations that have been created
78
79
  const filteredProjects = [];
79
80
  Object.entries(projects).forEach(([name, project]) => {
@@ -88,7 +89,7 @@ async function lintProjectGeneratorInternal(tree, options) {
88
89
  // additionally, the companion e2e app would have `rootProject: true`
89
90
  // so we need to check for the root path as well
90
91
  if (!options.rootProject || projectConfig.root !== '.') {
91
- createEsLintConfiguration(tree, projectConfig, options.setParserOptionsProject, options.rootProject);
92
+ createEsLintConfiguration(tree, options, projectConfig, options.setParserOptionsProject, options.rootProject);
92
93
  }
93
94
  // Buildable libs need source analysis enabled for linting `package.json`.
94
95
  if (isBuildableLibraryProject(projectConfig) &&
@@ -107,7 +108,7 @@ async function lintProjectGeneratorInternal(tree, options) {
107
108
  return (0, devkit_1.runTasksInSerial)(...tasks);
108
109
  }
109
110
  exports.lintProjectGeneratorInternal = lintProjectGeneratorInternal;
110
- function createEsLintConfiguration(tree, projectConfig, setParserOptionsProject, rootProject) {
111
+ function createEsLintConfiguration(tree, options, projectConfig, setParserOptionsProject, rootProject) {
111
112
  // we are only extending root for non-standalone projects or their complementary e2e apps
112
113
  const extendedRootConfig = rootProject ? undefined : (0, eslint_file_1.findEslintFile)(tree);
113
114
  const pathToRootConfig = extendedRootConfig
@@ -150,7 +151,8 @@ function createEsLintConfiguration(tree, projectConfig, setParserOptionsProject,
150
151
  rules: {},
151
152
  },
152
153
  ];
153
- if (isBuildableLibraryProject(projectConfig)) {
154
+ if (options.addPackageJsonDependencyChecks ||
155
+ isBuildableLibraryProject(projectConfig)) {
154
156
  overrides.push({
155
157
  files: ['*.json'],
156
158
  parser: 'jsonc-eslint-parser',
@@ -198,30 +200,24 @@ function isBuildableLibraryProject(projectConfig) {
198
200
  * Detect based on the state of lint target configuration of the root project
199
201
  * if we should migrate eslint configs to monorepo style
200
202
  */
201
- function isMigrationToMonorepoNeeded(projects, tree) {
203
+ function isMigrationToMonorepoNeeded(tree, graph) {
202
204
  // the base config is already created, migration has been done
203
205
  if (tree.exists(config_file_1.baseEsLintConfigFile) ||
204
206
  tree.exists(config_file_1.baseEsLintFlatConfigFile)) {
205
207
  return false;
206
208
  }
207
- const configs = Object.values(projects);
208
- if (configs.length === 1) {
209
- return false;
210
- }
209
+ const nodes = Object.values(graph.nodes);
211
210
  // get root project
212
- const rootProject = configs.find((p) => p.root === '.');
213
- if (!rootProject || !rootProject.targets) {
211
+ const rootProject = nodes.find((p) => p.data.root === '.');
212
+ if (!rootProject || !rootProject.data.targets) {
214
213
  return false;
215
214
  }
216
- // check if we're inferring lint target from `@nx/eslint/plugin`
217
- if ((0, plugin_1.hasEslintPlugin)(tree)) {
218
- for (const f of config_file_1.ESLINT_CONFIG_FILENAMES) {
219
- if (tree.exists(f)) {
220
- return true;
221
- }
215
+ for (const targetConfig of Object.values(rootProject.data.targets ?? {})) {
216
+ if (['@nx/eslint:lint', '@nrwl/linter:eslint', '@nx/linter:eslint'].includes(targetConfig.executor) ||
217
+ (targetConfig.executor === 'nx:run-commands' &&
218
+ targetConfig.options?.command.startsWith('eslint '))) {
219
+ return true;
222
220
  }
223
221
  }
224
- // find if root project has lint target
225
- const lintTarget = (0, init_migration_1.findLintTarget)(rootProject);
226
- return !!lintTarget;
222
+ return false;
227
223
  }
@@ -189,7 +189,7 @@ function replaceOverridesInLintConfig(tree, root, overrides) {
189
189
  content = (0, ast_utils_1.removeOverridesFromLintConfig)(content);
190
190
  overrides.forEach((override) => {
191
191
  const flatOverride = (0, ast_utils_1.generateFlatOverride)(override);
192
- (0, ast_utils_1.addBlockToFlatConfigExport)(content, flatOverride);
192
+ content = (0, ast_utils_1.addBlockToFlatConfigExport)(content, flatOverride);
193
193
  });
194
194
  tree.write(fileName, content);
195
195
  }
@@ -209,7 +209,9 @@ function addExtendsToLintConfig(tree, root, plugin) {
209
209
  const pluginExtends = (0, ast_utils_1.generatePluginExtendsElement)(plugins);
210
210
  let content = tree.read(fileName, 'utf8');
211
211
  content = (0, ast_utils_1.addCompatToFlatConfig)(content);
212
- tree.write(fileName, (0, ast_utils_1.addBlockToFlatConfigExport)(content, pluginExtends));
212
+ tree.write(fileName, (0, ast_utils_1.addBlockToFlatConfigExport)(content, pluginExtends, {
213
+ insertAtTheEnd: false,
214
+ }));
213
215
  }
214
216
  else {
215
217
  const fileName = (0, devkit_1.joinPathFragments)(root, '.eslintrc.json');
@@ -4,6 +4,11 @@ exports.generateAst = exports.mapFilePaths = exports.generateFlatOverride = expo
4
4
  const devkit_1 = require("@nx/devkit");
5
5
  const ts = require("typescript");
6
6
  const path_utils_1 = require("./path-utils");
7
+ /**
8
+ * Supports direct identifiers, and those nested within an object (of arbitrary depth)
9
+ * E.g. `...foo` and `...foo.bar.baz.qux` etc
10
+ */
11
+ const SPREAD_ELEMENTS_REGEXP = /\s*\.\.\.[a-zA-Z0-9_]+(\.[a-zA-Z0-9_]+)*,?\n?/g;
7
12
  /**
8
13
  * Remove all overrides from the config file
9
14
  */
@@ -58,11 +63,13 @@ function hasOverride(content, lookup) {
58
63
  let objSource;
59
64
  if (ts.isObjectLiteralExpression(node)) {
60
65
  objSource = node.getFullText();
66
+ // strip any spread elements
67
+ objSource = objSource.replace(SPREAD_ELEMENTS_REGEXP, '');
61
68
  }
62
69
  else {
63
70
  const fullNodeText = node['expression'].arguments[0].body.expression.getFullText();
64
71
  // strip any spread elements
65
- objSource = fullNodeText.replace(/\s*\.\.\.[a-zA-Z0-9_]+,?\n?/, '');
72
+ objSource = fullNodeText.replace(SPREAD_ELEMENTS_REGEXP, '');
66
73
  }
67
74
  const data = (0, devkit_1.parseJson)(objSource
68
75
  // ensure property names have double quotes so that JSON.parse works
@@ -76,7 +83,6 @@ function hasOverride(content, lookup) {
76
83
  return false;
77
84
  }
78
85
  exports.hasOverride = hasOverride;
79
- const STRIP_SPREAD_ELEMENTS = /\s*\.\.\.[a-zA-Z0-9_]+,?\n?/g;
80
86
  function parseTextToJson(text) {
81
87
  return (0, devkit_1.parseJson)(text
82
88
  // ensure property names have double quotes so that JSON.parse works
@@ -105,7 +111,7 @@ function replaceOverride(content, root, lookup, update) {
105
111
  else {
106
112
  const fullNodeText = node['expression'].arguments[0].body.expression.getFullText();
107
113
  // strip any spread elements
108
- objSource = fullNodeText.replace(STRIP_SPREAD_ELEMENTS, '');
114
+ objSource = fullNodeText.replace(SPREAD_ELEMENTS_REGEXP, '');
109
115
  start =
110
116
  node['expression'].arguments[0].body.expression.properties.pos +
111
117
  (fullNodeText.length - objSource.length);
@@ -283,7 +289,7 @@ function removePlugin(content, pluginName, pluginImport) {
283
289
  const pluginsArray = pluginsElem.initializer;
284
290
  const plugins = parseTextToJson(pluginsElem.initializer
285
291
  .getText()
286
- .replace(STRIP_SPREAD_ELEMENTS, ''));
292
+ .replace(SPREAD_ELEMENTS_REGEXP, ''));
287
293
  if (plugins.length > 1) {
288
294
  changes.push({
289
295
  type: devkit_1.ChangeType.Delete,
@@ -419,7 +425,7 @@ function addCompatToFlatConfig(content) {
419
425
  if (result.includes('const compat = new FlatCompat')) {
420
426
  return result;
421
427
  }
422
- result = addImportToFlatConfig(result, 'FlatCompat', '@eslint/eslintrc');
428
+ result = addImportToFlatConfig(result, ['FlatCompat'], '@eslint/eslintrc');
423
429
  const index = result.indexOf('module.exports');
424
430
  return (0, devkit_1.applyChangesToString)(result, [
425
431
  {
@@ -502,15 +508,33 @@ function generateFlatOverride(override) {
502
508
  !override.extends &&
503
509
  !override.plugins &&
504
510
  !override.parser) {
511
+ if (override.parserOptions) {
512
+ const { parserOptions, ...rest } = override;
513
+ return generateAst({ ...rest, languageOptions: { parserOptions } });
514
+ }
505
515
  return generateAst(override);
506
516
  }
507
- const { files, excludedFiles, rules, ...rest } = override;
517
+ const { files, excludedFiles, rules, parserOptions, ...rest } = override;
508
518
  const objectLiteralElements = [
509
519
  ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config')),
510
520
  ];
511
521
  addTSObjectProperty(objectLiteralElements, 'files', files);
512
522
  addTSObjectProperty(objectLiteralElements, 'excludedFiles', excludedFiles);
513
- addTSObjectProperty(objectLiteralElements, 'rules', rules);
523
+ // Apply rules (and spread ...config.rules into it as the first assignment)
524
+ addTSObjectProperty(objectLiteralElements, 'rules', rules || {});
525
+ const rulesObjectAST = objectLiteralElements.pop();
526
+ const rulesObjectInitializer = rulesObjectAST.initializer;
527
+ const spreadAssignment = ts.factory.createSpreadAssignment(ts.factory.createIdentifier('config.rules'));
528
+ const updatedRulesProperties = [
529
+ spreadAssignment,
530
+ ...rulesObjectInitializer.properties,
531
+ ];
532
+ objectLiteralElements.push(ts.factory.createPropertyAssignment('rules', ts.factory.createObjectLiteralExpression(updatedRulesProperties, true)));
533
+ if (parserOptions) {
534
+ addTSObjectProperty(objectLiteralElements, 'languageOptions', {
535
+ parserOptions,
536
+ });
537
+ }
514
538
  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, [
515
539
  ts.factory.createArrowFunction(undefined, undefined, [
516
540
  ts.factory.createParameterDeclaration(undefined, undefined, 'config'),
@@ -1,5 +1,7 @@
1
- import { CreateNodes } from '@nx/devkit';
1
+ import { CreateNodes, CreateNodesV2 } from '@nx/devkit';
2
2
  export interface EslintPluginOptions {
3
3
  targetName?: string;
4
+ extensions?: string[];
4
5
  }
6
+ export declare const createNodesV2: CreateNodesV2<EslintPluginOptions>;
5
7
  export declare const createNodes: CreateNodes<EslintPluginOptions>;
@@ -1,85 +1,169 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createNodes = void 0;
4
- const path_1 = require("path");
5
- const fs_1 = require("fs");
3
+ exports.createNodes = exports.createNodesV2 = void 0;
4
+ const devkit_1 = require("@nx/devkit");
5
+ const node_fs_1 = require("node:fs");
6
+ const node_path_1 = require("node:path");
6
7
  const globs_1 = require("nx/src/utils/globs");
8
+ const workspace_context_1 = require("nx/src/utils/workspace-context");
7
9
  const config_file_1 = require("../utils/config-file");
10
+ const resolve_eslint_class_1 = require("../utils/resolve-eslint-class");
11
+ const semver_1 = require("semver");
12
+ const cache_directory_1 = require("nx/src/utils/cache-directory");
13
+ const file_hasher_1 = require("nx/src/hasher/file-hasher");
14
+ const calculate_hash_for_create_nodes_1 = require("@nx/devkit/src/utils/calculate-hash-for-create-nodes");
15
+ const DEFAULT_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx', 'html', 'vue'];
16
+ const ESLINT_CONFIG_GLOB = (0, globs_1.combineGlobPatterns)([
17
+ ...config_file_1.ESLINT_CONFIG_FILENAMES.map((f) => `**/${f}`),
18
+ ]);
19
+ function readTargetsCache(cachePath) {
20
+ return (0, node_fs_1.existsSync)(cachePath) ? (0, devkit_1.readJsonFile)(cachePath) : {};
21
+ }
22
+ function writeTargetsToCache(cachePath, results) {
23
+ (0, devkit_1.writeJsonFile)(cachePath, results);
24
+ }
25
+ const internalCreateNodes = async (configFilePath, options, context, projectsCache) => {
26
+ options = normalizeOptions(options);
27
+ const configDir = (0, node_path_1.dirname)(configFilePath);
28
+ // Ensure that configFiles are set, e2e-run fails due to them being undefined in CI (does not occur locally)
29
+ // TODO(JamesHenry): Further troubleshoot this in CI
30
+ context.configFiles = context.configFiles ?? [];
31
+ // Create a Set of all the directories containing eslint configs, and a
32
+ // list of globs to exclude from child projects
33
+ const eslintRoots = new Set();
34
+ const nestedEslintRootPatterns = [];
35
+ for (const configFile of context.configFiles) {
36
+ const eslintRootDir = (0, node_path_1.dirname)(configFile);
37
+ eslintRoots.add(eslintRootDir);
38
+ if (eslintRootDir !== configDir && isSubDir(configDir, eslintRootDir)) {
39
+ nestedEslintRootPatterns.push(`${eslintRootDir}/**/*`);
40
+ }
41
+ }
42
+ const projectFiles = await (0, workspace_context_1.globWithWorkspaceContext)(context.workspaceRoot, ['project.json', 'package.json', '**/project.json', '**/package.json'].map((f) => (0, node_path_1.join)(configDir, f)), nestedEslintRootPatterns.length ? nestedEslintRootPatterns : undefined);
43
+ // dedupe and sort project roots by depth for more efficient traversal
44
+ const dedupedProjectRoots = Array.from(new Set(projectFiles.map((f) => (0, node_path_1.dirname)(f)))).sort((a, b) => (a !== b && isSubDir(a, b) ? -1 : 1));
45
+ const excludePatterns = dedupedProjectRoots.map((root) => `${root}/**/*`);
46
+ const ESLint = await (0, resolve_eslint_class_1.resolveESLintClass)((0, config_file_1.isFlatConfig)(configFilePath));
47
+ const eslintVersion = ESLint.version;
48
+ const childProjectRoots = new Set();
49
+ const projects = {};
50
+ await Promise.all(dedupedProjectRoots.map(async (childProjectRoot, index) => {
51
+ // anything after is either a nested project or a sibling project, can be excluded
52
+ const nestedProjectRootPatterns = excludePatterns.slice(index + 1);
53
+ // Ignore project roots where the project does not contain any lintable files
54
+ const lintableFiles = await (0, workspace_context_1.globWithWorkspaceContext)(context.workspaceRoot, [(0, node_path_1.join)(childProjectRoot, `**/*.{${options.extensions.join(',')}}`)],
55
+ // exclude nested eslint roots and nested project roots
56
+ [...nestedEslintRootPatterns, ...nestedProjectRootPatterns]);
57
+ const parentConfigs = context.configFiles.filter((eslintConfig) => isSubDir(childProjectRoot, (0, node_path_1.dirname)(eslintConfig)));
58
+ const hash = await (0, calculate_hash_for_create_nodes_1.calculateHashForCreateNodes)(childProjectRoot, options, context, [...parentConfigs, (0, node_path_1.join)(childProjectRoot, '.eslintignore')]);
59
+ if (projectsCache[hash]) {
60
+ // We can reuse the projects in the cache.
61
+ Object.assign(projects, projectsCache[hash]);
62
+ return;
63
+ }
64
+ const eslint = new ESLint({
65
+ cwd: (0, node_path_1.join)(context.workspaceRoot, childProjectRoot),
66
+ });
67
+ for (const file of lintableFiles) {
68
+ if (!(await eslint.isPathIgnored((0, node_path_1.join)(context.workspaceRoot, file)))) {
69
+ childProjectRoots.add(childProjectRoot);
70
+ break;
71
+ }
72
+ }
73
+ const uniqueChildProjectRoots = Array.from(childProjectRoots);
74
+ const projectsForRoot = getProjectsUsingESLintConfig(configFilePath, uniqueChildProjectRoots, eslintVersion, options, context);
75
+ if (Object.keys(projectsForRoot).length > 0) {
76
+ Object.assign(projects, projectsForRoot);
77
+ // Store those projects into the cache;
78
+ projectsCache[hash] = projectsForRoot;
79
+ }
80
+ }));
81
+ return {
82
+ projects,
83
+ };
84
+ };
85
+ exports.createNodesV2 = [
86
+ ESLINT_CONFIG_GLOB,
87
+ async (configFiles, options, context) => {
88
+ const optionsHash = (0, file_hasher_1.hashObject)(options);
89
+ const cachePath = (0, node_path_1.join)(cache_directory_1.workspaceDataDirectory, `eslint-${optionsHash}.hash`);
90
+ const targetsCache = readTargetsCache(cachePath);
91
+ try {
92
+ return await (0, devkit_1.createNodesFromFiles)((configFile, options, context) => internalCreateNodes(configFile, options, context, targetsCache), configFiles, options, context);
93
+ }
94
+ finally {
95
+ writeTargetsToCache(cachePath, targetsCache);
96
+ }
97
+ },
98
+ ];
8
99
  exports.createNodes = [
9
- (0, globs_1.combineGlobPatterns)(['**/project.json', '**/package.json']),
100
+ ESLINT_CONFIG_GLOB,
10
101
  (configFilePath, options, context) => {
11
- const projectRoot = (0, path_1.dirname)(configFilePath);
12
- options = normalizeOptions(options);
13
- const eslintConfigs = getEslintConfigsForProject(projectRoot, context.workspaceRoot);
14
- if (!eslintConfigs.length) {
15
- return {};
16
- }
17
- return {
18
- projects: {
19
- [projectRoot]: {
20
- targets: buildEslintTargets(eslintConfigs, projectRoot, options),
21
- },
22
- },
23
- };
102
+ devkit_1.logger.warn('`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.');
103
+ return internalCreateNodes(configFilePath, options, context, {});
24
104
  },
25
105
  ];
26
- function getEslintConfigsForProject(projectRoot, workspaceRoot) {
27
- const detectedConfigs = new Set();
28
- const baseConfig = (0, config_file_1.findBaseEslintFile)(workspaceRoot);
29
- if (baseConfig) {
30
- detectedConfigs.add(baseConfig);
31
- }
32
- let siblingFiles = (0, fs_1.readdirSync)((0, path_1.join)(workspaceRoot, projectRoot));
33
- if (projectRoot === '.') {
34
- // If there's no src folder, it's not a standalone project
35
- if (!siblingFiles.includes('src')) {
36
- return [];
106
+ function getProjectsUsingESLintConfig(configFilePath, childProjectRoots, eslintVersion, options, context) {
107
+ const projects = {};
108
+ const rootEslintConfig = [
109
+ config_file_1.baseEsLintConfigFile,
110
+ config_file_1.baseEsLintFlatConfigFile,
111
+ ...config_file_1.ESLINT_CONFIG_FILENAMES,
112
+ ].find((f) => (0, node_fs_1.existsSync)((0, node_path_1.join)(context.workspaceRoot, f)));
113
+ // Add a lint target for each child project without an eslint config, with the root level config as an input
114
+ for (const projectRoot of childProjectRoots) {
115
+ let standaloneSrcPath;
116
+ if (projectRoot === '.' &&
117
+ (0, node_fs_1.existsSync)((0, node_path_1.join)(context.workspaceRoot, projectRoot, 'package.json'))) {
118
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(context.workspaceRoot, projectRoot, 'src'))) {
119
+ standaloneSrcPath = 'src';
120
+ }
121
+ else if ((0, node_fs_1.existsSync)((0, node_path_1.join)(context.workspaceRoot, projectRoot, 'lib'))) {
122
+ standaloneSrcPath = 'lib';
123
+ }
37
124
  }
38
- // If it's standalone but doesn't have eslint config, it's not a lintable
39
- const config = siblingFiles.find((f) => config_file_1.ESLINT_CONFIG_FILENAMES.includes(f));
40
- if (!config) {
41
- return [];
125
+ if (projectRoot === '.' && !standaloneSrcPath) {
126
+ continue;
42
127
  }
43
- detectedConfigs.add(config);
44
- return Array.from(detectedConfigs);
45
- }
46
- while (projectRoot !== '.') {
47
- // if it has an eslint config it's lintable
48
- const config = siblingFiles.find((f) => config_file_1.ESLINT_CONFIG_FILENAMES.includes(f));
49
- if (config) {
50
- detectedConfigs.add(`${projectRoot}/${config}`);
51
- return Array.from(detectedConfigs);
128
+ const eslintConfigs = [configFilePath];
129
+ if (rootEslintConfig && !eslintConfigs.includes(rootEslintConfig)) {
130
+ eslintConfigs.unshift(rootEslintConfig);
52
131
  }
53
- projectRoot = (0, path_1.dirname)(projectRoot);
54
- siblingFiles = (0, fs_1.readdirSync)((0, path_1.join)(workspaceRoot, projectRoot));
55
- }
56
- // check whether the root has an eslint config
57
- const config = (0, fs_1.readdirSync)(workspaceRoot).find((f) => config_file_1.ESLINT_CONFIG_FILENAMES.includes(f));
58
- if (config) {
59
- detectedConfigs.add(config);
60
- return Array.from(detectedConfigs);
132
+ projects[projectRoot] = {
133
+ targets: buildEslintTargets(eslintConfigs, eslintVersion, projectRoot, context.workspaceRoot, options, standaloneSrcPath),
134
+ };
61
135
  }
62
- return [];
136
+ return projects;
63
137
  }
64
- function buildEslintTargets(eslintConfigs, projectRoot, options) {
138
+ function buildEslintTargets(eslintConfigs, eslintVersion, projectRoot, workspaceRoot, options, standaloneSrcPath) {
65
139
  const isRootProject = projectRoot === '.';
66
140
  const targets = {};
67
141
  const targetConfig = {
68
- command: `eslint ${isRootProject ? './src' : '.'}`,
142
+ command: `eslint ${isRootProject && standaloneSrcPath ? `./${standaloneSrcPath}` : '.'}`,
69
143
  cache: true,
70
144
  options: {
71
145
  cwd: projectRoot,
72
146
  },
73
147
  inputs: [
74
148
  'default',
75
- ...eslintConfigs.map((config) => `{workspaceRoot}/${config}`),
149
+ // Certain lint rules can be impacted by changes to dependencies
150
+ '^default',
151
+ ...eslintConfigs.map((config) => `{workspaceRoot}/${config}`.replace(`{workspaceRoot}/${projectRoot}`, isRootProject ? '{projectRoot}/' : '{projectRoot}')),
152
+ ...((0, node_fs_1.existsSync)((0, node_path_1.join)(workspaceRoot, projectRoot, '.eslintignore'))
153
+ ? ['{projectRoot}/.eslintignore']
154
+ : []),
76
155
  '{workspaceRoot}/tools/eslint-rules/**/*',
77
156
  { externalDependencies: ['eslint'] },
78
157
  ],
158
+ outputs: ['{options.outputFile}'],
79
159
  };
80
- if (eslintConfigs.some((config) => (0, config_file_1.isFlatConfig)(config))) {
160
+ // Always set the environment variable to ensure that the ESLint CLI can run on eslint v8 and v9
161
+ const useFlatConfig = eslintConfigs.some((config) => (0, config_file_1.isFlatConfig)(config));
162
+ // Flat config is default for 9.0.0+
163
+ const defaultSetting = (0, semver_1.gte)(eslintVersion, '9.0.0');
164
+ if (useFlatConfig !== defaultSetting) {
81
165
  targetConfig.options.env = {
82
- ESLINT_USE_FLAT_CONFIG: 'true',
166
+ ESLINT_USE_FLAT_CONFIG: useFlatConfig ? 'true' : 'false',
83
167
  };
84
168
  }
85
169
  targets[options.targetName] = targetConfig;
@@ -88,5 +172,28 @@ function buildEslintTargets(eslintConfigs, projectRoot, options) {
88
172
  function normalizeOptions(options) {
89
173
  options ??= {};
90
174
  options.targetName ??= 'lint';
175
+ // Normalize user input for extensions (strip leading . characters)
176
+ if (Array.isArray(options.extensions)) {
177
+ options.extensions = options.extensions.map((f) => f.replace(/^\.+/, ''));
178
+ }
179
+ else {
180
+ options.extensions = DEFAULT_EXTENSIONS;
181
+ }
91
182
  return options;
92
183
  }
184
+ /**
185
+ * Determines if `child` is a subdirectory of `parent`. This is a simplified
186
+ * version that takes into account that paths are always relative to the
187
+ * workspace root.
188
+ */
189
+ function isSubDir(parent, child) {
190
+ if (parent === '.') {
191
+ return true;
192
+ }
193
+ parent = (0, node_path_1.normalize)(parent);
194
+ child = (0, node_path_1.normalize)(child);
195
+ if (!parent.endsWith(node_path_1.sep)) {
196
+ parent += node_path_1.sep;
197
+ }
198
+ return child.startsWith(parent);
199
+ }
@@ -1,5 +1,8 @@
1
+ export declare const ESLINT_FLAT_CONFIG_FILENAMES: string[];
2
+ export declare const ESLINT_OLD_CONFIG_FILENAMES: string[];
1
3
  export declare const ESLINT_CONFIG_FILENAMES: string[];
2
4
  export declare const baseEsLintConfigFile = ".eslintrc.base.json";
3
5
  export declare const baseEsLintFlatConfigFile = "eslint.base.config.js";
4
- export declare function findBaseEslintFile(workspaceRoot?: string): string | null;
5
6
  export declare function isFlatConfig(configFilePath: string): boolean;
7
+ export declare function findFlatConfigFile(directory: string, workspaceRoot: string): string | null;
8
+ export declare function findOldConfigFile(filePathOrDirectory: string, workspaceRoot: string): string | null;
@@ -1,35 +1,69 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isFlatConfig = exports.findBaseEslintFile = exports.baseEsLintFlatConfigFile = exports.baseEsLintConfigFile = exports.ESLINT_CONFIG_FILENAMES = void 0;
4
- const devkit_1 = require("@nx/devkit");
3
+ exports.findOldConfigFile = exports.findFlatConfigFile = exports.isFlatConfig = exports.baseEsLintFlatConfigFile = exports.baseEsLintConfigFile = exports.ESLINT_CONFIG_FILENAMES = exports.ESLINT_OLD_CONFIG_FILENAMES = exports.ESLINT_FLAT_CONFIG_FILENAMES = void 0;
5
4
  const fs_1 = require("fs");
6
- exports.ESLINT_CONFIG_FILENAMES = [
5
+ const path_1 = require("path");
6
+ // TODO(leo): add support for eslint.config.mjs and eslint.config.cjs
7
+ exports.ESLINT_FLAT_CONFIG_FILENAMES = ['eslint.config.js'];
8
+ exports.ESLINT_OLD_CONFIG_FILENAMES = [
7
9
  '.eslintrc',
8
10
  '.eslintrc.js',
9
11
  '.eslintrc.cjs',
10
12
  '.eslintrc.yaml',
11
13
  '.eslintrc.yml',
12
14
  '.eslintrc.json',
13
- 'eslint.config.js',
15
+ ];
16
+ exports.ESLINT_CONFIG_FILENAMES = [
17
+ ...exports.ESLINT_OLD_CONFIG_FILENAMES,
18
+ ...exports.ESLINT_FLAT_CONFIG_FILENAMES,
14
19
  ];
15
20
  exports.baseEsLintConfigFile = '.eslintrc.base.json';
16
21
  exports.baseEsLintFlatConfigFile = 'eslint.base.config.js';
17
- function findBaseEslintFile(workspaceRoot = '') {
18
- if ((0, fs_1.existsSync)((0, devkit_1.joinPathFragments)(workspaceRoot, exports.baseEsLintConfigFile))) {
19
- return exports.baseEsLintConfigFile;
22
+ function isFlatConfig(configFilePath) {
23
+ const configFileName = (0, path_1.basename)(configFilePath);
24
+ return exports.ESLINT_FLAT_CONFIG_FILENAMES.includes(configFileName);
25
+ }
26
+ exports.isFlatConfig = isFlatConfig;
27
+ // https://eslint.org/docs/latest/use/configure/configuration-files#configuration-file-resolution
28
+ function findFlatConfigFile(directory, workspaceRoot) {
29
+ let currentDir = (0, path_1.resolve)(workspaceRoot, directory);
30
+ while (true) {
31
+ const configFilePath = getConfigFileInDirectory(currentDir, exports.ESLINT_FLAT_CONFIG_FILENAMES);
32
+ if (configFilePath) {
33
+ return configFilePath;
34
+ }
35
+ if (currentDir === workspaceRoot) {
36
+ break;
37
+ }
38
+ currentDir = (0, path_1.dirname)(currentDir);
20
39
  }
21
- if ((0, fs_1.existsSync)((0, devkit_1.joinPathFragments)(workspaceRoot, exports.baseEsLintFlatConfigFile))) {
22
- return exports.baseEsLintFlatConfigFile;
40
+ return null;
41
+ }
42
+ exports.findFlatConfigFile = findFlatConfigFile;
43
+ function findOldConfigFile(filePathOrDirectory, workspaceRoot) {
44
+ let currentDir = (0, path_1.resolve)(workspaceRoot, filePathOrDirectory);
45
+ if (!(0, fs_1.statSync)(currentDir).isDirectory()) {
46
+ currentDir = (0, path_1.dirname)(currentDir);
23
47
  }
24
- for (const file of exports.ESLINT_CONFIG_FILENAMES) {
25
- if ((0, fs_1.existsSync)((0, devkit_1.joinPathFragments)(workspaceRoot, file))) {
26
- return file;
48
+ while (true) {
49
+ const configFilePath = getConfigFileInDirectory(currentDir, exports.ESLINT_OLD_CONFIG_FILENAMES);
50
+ if (configFilePath) {
51
+ return configFilePath;
27
52
  }
53
+ if (currentDir === workspaceRoot) {
54
+ break;
55
+ }
56
+ currentDir = (0, path_1.dirname)(currentDir);
28
57
  }
29
58
  return null;
30
59
  }
31
- exports.findBaseEslintFile = findBaseEslintFile;
32
- function isFlatConfig(configFilePath) {
33
- return configFilePath.endsWith('.config.js');
60
+ exports.findOldConfigFile = findOldConfigFile;
61
+ function getConfigFileInDirectory(directory, candidateFileNames) {
62
+ for (const filename of candidateFileNames) {
63
+ const filePath = (0, path_1.join)(directory, filename);
64
+ if ((0, fs_1.existsSync)(filePath)) {
65
+ return filePath;
66
+ }
67
+ }
68
+ return null;
34
69
  }
35
- exports.isFlatConfig = isFlatConfig;
@@ -0,0 +1,2 @@
1
+ import type { ESLint } from 'eslint';
2
+ export declare function resolveESLintClass(useFlatConfig?: boolean): Promise<typeof ESLint>;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveESLintClass = void 0;
4
+ async function resolveESLintClass(useFlatConfig = false) {
5
+ try {
6
+ // In eslint 8.57.0 (the final v8 version), a dedicated API was added for resolving the correct ESLint class.
7
+ const eslint = await Promise.resolve().then(() => require('eslint'));
8
+ if (typeof eslint.loadESLint === 'function') {
9
+ return await eslint.loadESLint({ useFlatConfig });
10
+ }
11
+ // If that API is not available (an older version of v8), we need to use the old way of resolving the ESLint class.
12
+ if (!useFlatConfig) {
13
+ return eslint.ESLint;
14
+ }
15
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
16
+ const { FlatESLint } = require('eslint/use-at-your-own-risk');
17
+ return FlatESLint;
18
+ }
19
+ catch {
20
+ throw new Error('Unable to find ESLint. Ensure ESLint is installed.');
21
+ }
22
+ }
23
+ exports.resolveESLintClass = resolveESLintClass;
@@ -1,5 +1,5 @@
1
1
  export declare const nxVersion: any;
2
- export declare const eslintVersion = "~8.48.0";
2
+ export declare const eslintVersion = "~8.57.0";
3
3
  export declare const eslintrcVersion = "^2.1.1";
4
4
  export declare const eslintConfigPrettierVersion = "^9.0.0";
5
- export declare const typescriptESLintVersion = "^6.13.2";
5
+ export declare const typescriptESLintVersion = "^7.3.0";
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.typescriptESLintVersion = exports.eslintConfigPrettierVersion = exports.eslintrcVersion = exports.eslintVersion = exports.nxVersion = void 0;
4
4
  exports.nxVersion = require('../../package.json').version;
5
- exports.eslintVersion = '~8.48.0';
5
+ exports.eslintVersion = '~8.57.0';
6
6
  exports.eslintrcVersion = '^2.1.1';
7
7
  exports.eslintConfigPrettierVersion = '^9.0.0';
8
- exports.typescriptESLintVersion = '^6.13.2';
8
+ exports.typescriptESLintVersion = '^7.3.0';
@@ -1,2 +0,0 @@
1
- import { Tree } from '@nx/devkit';
2
- export default function addEslintInputs(tree: Tree): Promise<void>;
@@ -1,27 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const devkit_1 = require("@nx/devkit");
4
- const eslint_targets_1 = require("../../generators/utils/eslint-targets");
5
- const config_file_1 = require("../../utils/config-file");
6
- async function addEslintInputs(tree) {
7
- const nxJson = (0, devkit_1.readNxJson)(tree);
8
- const globalEslintFile = config_file_1.ESLINT_CONFIG_FILENAMES.find((file) => tree.exists(file));
9
- if (globalEslintFile && nxJson.namedInputs?.production) {
10
- const productionFileset = new Set(nxJson.namedInputs.production);
11
- productionFileset.add(`!{projectRoot}/${globalEslintFile}`);
12
- nxJson.namedInputs.production = Array.from(productionFileset);
13
- }
14
- for (const targetName of (0, eslint_targets_1.getEslintTargets)(tree)) {
15
- nxJson.targetDefaults ??= {};
16
- const lintTargetDefaults = (nxJson.targetDefaults[targetName] ??= {});
17
- lintTargetDefaults.inputs ??= [
18
- 'default',
19
- ...(globalEslintFile
20
- ? [(0, devkit_1.joinPathFragments)('{workspaceRoot}', globalEslintFile)]
21
- : []),
22
- ];
23
- }
24
- (0, devkit_1.updateNxJson)(tree, nxJson);
25
- await (0, devkit_1.formatFiles)(tree);
26
- }
27
- exports.default = addEslintInputs;
@@ -1,2 +0,0 @@
1
- import { Tree } from '@nx/devkit';
2
- export default function addEslintIgnore(tree: Tree): Promise<void>;
@@ -1,36 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const devkit_1 = require("@nx/devkit");
4
- const eslint_targets_1 = require("../../generators/utils/eslint-targets");
5
- const config_file_1 = require("../../utils/config-file");
6
- async function addEslintIgnore(tree) {
7
- const nxJson = (0, devkit_1.readJson)(tree, 'nx.json');
8
- const globalEslintFile = config_file_1.ESLINT_CONFIG_FILENAMES.find((file) => tree.exists(file));
9
- if (globalEslintFile) {
10
- if (tree.exists('.eslintignore')) {
11
- const content = tree.read('.eslintignore', 'utf-8');
12
- if (!content.includes('node_modules')) {
13
- tree.write('.eslintignore', `node_modules\n${content}`);
14
- }
15
- }
16
- else {
17
- tree.write('.eslintignore', 'node_modules\n');
18
- }
19
- for (const targetName of (0, eslint_targets_1.getEslintTargets)(tree)) {
20
- nxJson.targetDefaults ??= {};
21
- const lintTargetDefaults = (nxJson.targetDefaults[targetName] ??= {});
22
- const lintIgnorePath = (0, devkit_1.joinPathFragments)('{workspaceRoot}', globalEslintFile);
23
- if (lintTargetDefaults.inputs) {
24
- if (!lintTargetDefaults.inputs.includes(lintIgnorePath)) {
25
- lintTargetDefaults.inputs.push(lintIgnorePath);
26
- }
27
- }
28
- else {
29
- lintTargetDefaults.inputs = ['default', lintIgnorePath];
30
- }
31
- }
32
- (0, devkit_1.updateNxJson)(tree, nxJson);
33
- await (0, devkit_1.formatFiles)(tree);
34
- }
35
- }
36
- exports.default = addEslintIgnore;