Skip to content

Commit d78782d

Browse files
authored
fix(linter): speed up inferred plugin node processing (#31281)
This PR improves the **createNodes** function of eslint's inferred plugin by making two pragmatic choices: - reusing the ESLint between config file's runs instead of recreating the new one every time - skipping ignored files checks for projects that already have eslint config file ## Results of benchmarks on customer's repo: ### Without ESLint plugin - create-project-graph-async - avg. 11739.1326225 -> 11 seconds ### With current ESLint plugin - create-project-graph-async - avg. 98005.0965135 -> 98 seconds ### With modified ESLint plugin - create-project-graph-async - avg. 13225.073817 -> 13 seconds - (@nx/eslint/plugin:createNodes - 2206.96497, 16.69%) ## Current Behavior <!-- This is the behavior we have today --> ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
1 parent 3a33d5f commit d78782d

File tree

2 files changed

+60
-48
lines changed

2 files changed

+60
-48
lines changed

packages/eslint/src/plugins/plugin.spec.ts

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -518,43 +518,45 @@ describe('@nx/eslint/plugin', () => {
518518
`);
519519
});
520520

521-
it('should not create nodes for nested projects without a root level eslint config when all files are ignored (.eslintignore)', async () => {
522-
createFiles({
523-
'apps/my-app/.eslintrc.json': `{}`,
524-
'apps/my-app/.eslintignore': `**/*`,
525-
'apps/my-app/project.json': `{}`,
526-
'apps/my-app/index.ts': `console.log('hello world')`,
527-
'libs/my-lib/.eslintrc.json': `{}`,
528-
'libs/my-lib/.eslintignore': `**/*`,
529-
'libs/my-lib/project.json': `{}`,
530-
'libs/my-lib/index.ts': `console.log('hello world')`,
531-
});
532-
expect(
533-
await invokeCreateNodesOnMatchingFiles(context, { targetName: 'lint' })
534-
).toMatchInlineSnapshot(`
535-
{
536-
"projects": {},
537-
}
538-
`);
539-
});
521+
// This is intentionally disabled, since we should always create a node for project that contains eslint config
522+
// it('should not create nodes for nested projects without a root level eslint config when all files are ignored (.eslintignore)', async () => {
523+
// createFiles({
524+
// 'apps/my-app/.eslintrc.json': `{}`,
525+
// 'apps/my-app/.eslintignore': `**/*`,
526+
// 'apps/my-app/project.json': `{}`,
527+
// 'apps/my-app/index.ts': `console.log('hello world')`,
528+
// 'libs/my-lib/.eslintrc.json': `{}`,
529+
// 'libs/my-lib/.eslintignore': `**/*`,
530+
// 'libs/my-lib/project.json': `{}`,
531+
// 'libs/my-lib/index.ts': `console.log('hello world')`,
532+
// });
533+
// expect(
534+
// await invokeCreateNodesOnMatchingFiles(context, { targetName: 'lint' })
535+
// ).toMatchInlineSnapshot(`
536+
// {
537+
// "projects": {},
538+
// }
539+
// `);
540+
// });
540541

541-
it('should not create nodes for nested projects without a root level eslint config when all files are ignored (ignorePatterns in .eslintrc.json)', async () => {
542-
createFiles({
543-
'apps/my-app/.eslintrc.json': `{ "ignorePatterns": ["**/*"] }`,
544-
'apps/my-app/project.json': `{}`,
545-
'apps/my-app/index.ts': `console.log('hello world')`,
546-
'libs/my-lib/.eslintrc.json': `{ "ignorePatterns": ["**/*"] }`,
547-
'libs/my-lib/project.json': `{}`,
548-
'libs/my-lib/index.ts': `console.log('hello world')`,
549-
});
550-
expect(
551-
await invokeCreateNodesOnMatchingFiles(context, { targetName: 'lint' })
552-
).toMatchInlineSnapshot(`
553-
{
554-
"projects": {},
555-
}
556-
`);
557-
});
542+
// This is intentionally disabled, since we should always create a node for project that contains eslint config
543+
// it('should not create nodes for nested projects without a root level eslint config when all files are ignored (ignorePatterns in .eslintrc.json)', async () => {
544+
// createFiles({
545+
// 'apps/my-app/.eslintrc.json': `{ "ignorePatterns": ["**/*"] }`,
546+
// 'apps/my-app/project.json': `{}`,
547+
// 'apps/my-app/index.ts': `console.log('hello world')`,
548+
// 'libs/my-lib/.eslintrc.json': `{ "ignorePatterns": ["**/*"] }`,
549+
// 'libs/my-lib/project.json': `{}`,
550+
// 'libs/my-lib/index.ts': `console.log('hello world')`,
551+
// });
552+
// expect(
553+
// await invokeCreateNodesOnMatchingFiles(context, { targetName: 'lint' })
554+
// ).toMatchInlineSnapshot(`
555+
// {
556+
// "projects": {},
557+
// }
558+
// `);
559+
// });
558560
});
559561

560562
describe('root eslint config and nested eslint configs', () => {
@@ -721,7 +723,6 @@ describe('@nx/eslint/plugin', () => {
721723
it('should handle multiple levels of nesting and ignored files correctly', async () => {
722724
createFiles({
723725
'.eslintrc.json': '{ "root": true, "ignorePatterns": ["**/*"] }',
724-
'apps/myapp/.eslintrc.json': '{ "extends": "../../.eslintrc.json" }', // no lintable files, don't create task
725726
'apps/myapp/project.json': '{}',
726727
'apps/myapp/index.ts': 'console.log("hello world")',
727728
'apps/myapp/nested/mylib/.eslintrc.json': JSON.stringify({

packages/eslint/src/plugins/plugin.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
2222
import { combineGlobPatterns } from 'nx/src/utils/globs';
2323
import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context';
2424
import { gte } from 'semver';
25+
import type { ESLint as ESLintType } from 'eslint';
2526
import {
2627
baseEsLintConfigFile,
2728
BASE_ESLINT_CONFIG_FILENAMES,
@@ -186,6 +187,7 @@ const internalCreateNodes = async (
186187
};
187188

188189
const internalCreateNodesV2 = async (
190+
ESLint: typeof ESLintType,
189191
configFilePath: string,
190192
options: EslintPluginOptions,
191193
context: CreateNodesContextV2,
@@ -195,10 +197,6 @@ const internalCreateNodesV2 = async (
195197
hashByRoot: Map<string, string>
196198
): Promise<CreateNodesResult> => {
197199
const configDir = dirname(configFilePath);
198-
199-
const ESLint = await resolveESLintClass({
200-
useFlatConfigOverrideVal: isFlatConfig(configFilePath),
201-
});
202200
const eslintVersion = ESLint.version;
203201

204202
const projects: CreateNodesResult['projects'] = {};
@@ -212,15 +210,21 @@ const internalCreateNodesV2 = async (
212210
return;
213211
}
214212

215-
const eslint = new ESLint({
216-
cwd: join(context.workspaceRoot, projectRoot),
217-
});
218213
let hasNonIgnoredLintableFiles = false;
219-
for (const file of lintableFilesPerProjectRoot.get(projectRoot) ?? []) {
220-
if (!(await eslint.isPathIgnored(join(context.workspaceRoot, file)))) {
221-
hasNonIgnoredLintableFiles = true;
222-
break;
214+
if (configDir !== projectRoot || projectRoot === '.') {
215+
const eslint = new ESLint({
216+
cwd: join(context.workspaceRoot, projectRoot),
217+
});
218+
for (const file of lintableFilesPerProjectRoot.get(projectRoot) ?? []) {
219+
if (
220+
!(await eslint.isPathIgnored(join(context.workspaceRoot, file)))
221+
) {
222+
hasNonIgnoredLintableFiles = true;
223+
break;
224+
}
223225
}
226+
} else {
227+
hasNonIgnoredLintableFiles = true;
224228
}
225229

226230
if (!hasNonIgnoredLintableFiles) {
@@ -286,9 +290,16 @@ export const createNodesV2: CreateNodesV2<EslintPluginOptions> = [
286290
projectRoots.map((r, i) => [r, hashes[i]])
287291
);
288292
try {
293+
if (eslintConfigFiles.length === 0) {
294+
return [];
295+
}
296+
const ESLint = await resolveESLintClass({
297+
useFlatConfigOverrideVal: isFlatConfig(eslintConfigFiles[0]),
298+
});
289299
return await createNodesFromFiles(
290300
(configFile, options, context) =>
291301
internalCreateNodesV2(
302+
ESLint,
292303
configFile,
293304
options,
294305
context,

0 commit comments

Comments
 (0)