Skip to content

Commit 29cd887

Browse files
committed
feat(plugin-eslint): provide Nx helper to combine eslint configs from project with deps
1 parent 6c1edb0 commit 29cd887

File tree

6 files changed

+206
-47
lines changed

6 files changed

+206
-47
lines changed

packages/plugin-eslint/src/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ export default eslintPlugin;
44

55
export type { ESLintPluginConfig } from './lib/config';
66

7-
export { eslintConfigFromNxProjects } from './lib/nx';
7+
export {
8+
eslintConfigFromNxProject,
9+
eslintConfigFromNxProjects,
10+
} from './lib/nx';

packages/plugin-eslint/src/lib/nx.integration.test.ts

+55-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { dirname, join } from 'path';
33
import { fileURLToPath } from 'url';
44
import type { SpyInstance } from 'vitest';
55
import { ESLintPluginConfig } from './config';
6-
import { eslintConfigFromNxProjects } from './nx';
6+
import { eslintConfigFromNxProject, eslintConfigFromNxProjects } from './nx';
77

88
describe('Nx helpers', () => {
99
let cwdSpy: SpyInstance;
@@ -89,4 +89,58 @@ describe('Nx helpers', () => {
8989
} satisfies ESLintPluginConfig);
9090
});
9191
});
92+
93+
describe('create config from target Nx project and its dependencies', () => {
94+
/*
95+
* Project graph:
96+
*
97+
* cli
98+
* │
99+
* │
100+
* ▼
101+
* core
102+
* │ nx-plugin
103+
* │ │
104+
* ▼ │
105+
* utils ◄──────┘
106+
*/
107+
108+
const allProjects = ['cli', 'core', 'nx-plugin', 'utils'] as const;
109+
type Project = (typeof allProjects)[number];
110+
111+
it.each<[Project, Project[]]>([
112+
['cli', ['cli', 'core', 'utils']],
113+
['core', ['core', 'utils']],
114+
['nx-plugin', ['nx-plugin', 'utils']],
115+
['utils', ['utils']],
116+
])(
117+
'project %j - expected configurations for projects %j',
118+
async (project, expectedProjects) => {
119+
const otherProjects = allProjects.filter(
120+
p => !expectedProjects.includes(p),
121+
);
122+
123+
const config = await eslintConfigFromNxProject(project);
124+
125+
expect(config.eslintrc).toEqual({
126+
root: true,
127+
overrides: expectedProjects.map(p => ({
128+
files: expect.arrayContaining([`packages/${p}/**/*.ts`]),
129+
extends: `./packages/${p}/.eslintrc.json`,
130+
})),
131+
});
132+
133+
expect(config.patterns).toEqual(
134+
expect.arrayContaining(
135+
expectedProjects.map(p => `packages/${p}/**/*.ts`),
136+
),
137+
);
138+
expect(config.patterns).toEqual(
139+
expect.not.arrayContaining(
140+
otherProjects.map(p => `packages/${p}/**/*.ts`),
141+
),
142+
);
143+
},
144+
);
145+
});
92146
});
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,29 @@
1-
import {
2-
createProjectGraphAsync,
3-
readProjectsConfigurationFromProjectGraph,
4-
} from '@nx/devkit';
5-
import type { ESLint } from 'eslint';
1+
import { createProjectGraphAsync } from '@nx/devkit';
62
import type { ESLintPluginConfig } from '../config';
7-
import {
8-
findCodePushupEslintrc,
9-
getEslintConfig,
10-
getLintFilePatterns,
11-
} from './utils';
3+
import { nxProjectsToConfig } from './projects-to-config';
124

5+
/**
6+
* Finds all Nx projects in workspace and converts their lint configurations to Code PushUp ESLint plugin parameters.
7+
*
8+
* Use when you wish to automatically include every Nx project in a single Code PushUp project.
9+
* If you prefer to only include a subset of your Nx monorepo, refer to {@link eslintConfigFromNxProject} instead.
10+
*
11+
* @example
12+
* import eslintPlugin, {
13+
* eslintConfigFromNxProjects,
14+
* } from '@code-pushup/eslint-plugin';
15+
*
16+
* export default {
17+
* plugins: [
18+
* await eslintPlugin(
19+
* await eslintConfigFromNxProjects()
20+
* )
21+
* ]
22+
* }
23+
*
24+
* @returns ESLint config and patterns, intended to be passed to {@link eslintPlugin}
25+
*/
1326
export async function eslintConfigFromNxProjects(): Promise<ESLintPluginConfig> {
14-
// find Nx projects with lint target
1527
const projectGraph = await createProjectGraphAsync({ exitOnError: false });
16-
const projectsConfiguration =
17-
readProjectsConfigurationFromProjectGraph(projectGraph);
18-
const projects = Object.values(projectsConfiguration.projects)
19-
.filter(project => 'lint' in (project.targets ?? {}))
20-
.sort((a, b) => a.root.localeCompare(b.root));
21-
22-
// create single ESLint config with project-specific overrides
23-
const eslintConfig: ESLint.ConfigData = {
24-
root: true,
25-
overrides: await Promise.all(
26-
projects.map(async project => ({
27-
files: getLintFilePatterns(project),
28-
extends:
29-
(await findCodePushupEslintrc(project)) ?? getEslintConfig(project),
30-
})),
31-
),
32-
};
33-
34-
// include patterns from each project
35-
const patterns = projects.flatMap(project => [
36-
...getLintFilePatterns(project),
37-
// HACK: ESLint.calculateConfigForFile won't find rules included only for subsets of *.ts when globs used
38-
// so we explicitly provide additional patterns used by @code-pushup/eslint-config to ensure those rules are included
39-
// this workaround won't be necessary once flat configs are stable (much easier to find all rules)
40-
`${project.sourceRoot}/*.spec.ts`, // jest/* and vitest/* rules
41-
`${project.sourceRoot}/*.cy.ts`, // cypress/* rules
42-
`${project.sourceRoot}/*.stories.ts`, // storybook/* rules
43-
`${project.sourceRoot}/.storybook/main.ts`, // storybook/no-uninstalled-addons rule
44-
]);
45-
46-
return {
47-
eslintrc: eslintConfig,
48-
patterns,
49-
};
28+
return nxProjectsToConfig(projectGraph);
5029
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { ProjectGraph, createProjectGraphAsync } from '@nx/devkit';
2+
import type { ESLintPluginConfig } from '../config';
3+
import { nxProjectsToConfig } from './projects-to-config';
4+
5+
/**
6+
* Accepts a target Nx projects, finds projects it depends on, and converts lint configurations to Code PushUp ESLint plugin parameters.
7+
*
8+
* Use when you wish to include a targetted subset of your Nx monorepo in your Code PushUp project.
9+
* If you prefer to include all Nx projects, refer to {@link eslintConfigFromNxProjects} instead.
10+
*
11+
* @example
12+
* import eslintPlugin, {
13+
* eslintConfigFromNxProject,
14+
* } from '@code-pushup/eslint-plugin';
15+
*
16+
* const projectName = 'backoffice'; // <-- name from project.json
17+
*
18+
* export default {
19+
* plugins: [
20+
* await eslintPlugin(
21+
* await eslintConfigFromNxProject(projectName)
22+
* )
23+
* ]
24+
* }
25+
*
26+
* @param projectName Nx project serving as main entry point
27+
* @returns ESLint config and patterns, intended to be passed to {@link eslintPlugin}
28+
*/
29+
export async function eslintConfigFromNxProject(
30+
projectName: string,
31+
): Promise<ESLintPluginConfig> {
32+
const projectGraph = await createProjectGraphAsync({ exitOnError: false });
33+
34+
const dependencies = findAllDependencies(projectName, projectGraph);
35+
36+
return nxProjectsToConfig(
37+
projectGraph,
38+
project =>
39+
!!project.name &&
40+
(project.name === projectName || dependencies.has(project.name)),
41+
);
42+
}
43+
44+
function findAllDependencies(
45+
name: string,
46+
projectGraph: ProjectGraph,
47+
): ReadonlySet<string> {
48+
const results = new Set<string>();
49+
const queue = [name];
50+
51+
// eslint-disable-next-line functional/no-loop-statements
52+
while (queue.length > 0) {
53+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
54+
const source = queue.shift()!;
55+
const dependencies = projectGraph.dependencies[source];
56+
57+
// eslint-disable-next-line functional/no-loop-statements
58+
for (const { target } of dependencies ?? []) {
59+
// skip duplicates (cycle in graph)
60+
if (!results.has(target)) {
61+
results.add(target);
62+
queue.push(target);
63+
}
64+
}
65+
}
66+
67+
return results;
68+
}
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { eslintConfigFromNxProjects } from './find-all-projects';
2+
export { eslintConfigFromNxProject } from './find-project-with-deps';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {
2+
ProjectConfiguration,
3+
ProjectGraph,
4+
readProjectsConfigurationFromProjectGraph,
5+
} from '@nx/devkit';
6+
import type { ESLint } from 'eslint';
7+
import type { ESLintPluginConfig } from '../config';
8+
import {
9+
findCodePushupEslintrc,
10+
getEslintConfig,
11+
getLintFilePatterns,
12+
} from './utils';
13+
14+
export async function nxProjectsToConfig(
15+
projectGraph: ProjectGraph,
16+
predicate: (project: ProjectConfiguration) => boolean = () => true,
17+
): Promise<ESLintPluginConfig> {
18+
// find Nx projects with lint target
19+
const projectsConfiguration =
20+
readProjectsConfigurationFromProjectGraph(projectGraph);
21+
const projects = Object.values(projectsConfiguration.projects)
22+
.filter(project => 'lint' in (project.targets ?? {}))
23+
.filter(predicate) // apply predicate
24+
.sort((a, b) => a.root.localeCompare(b.root));
25+
26+
// create single ESLint config with project-specific overrides
27+
const eslintConfig: ESLint.ConfigData = {
28+
root: true,
29+
overrides: await Promise.all(
30+
projects.map(async project => ({
31+
files: getLintFilePatterns(project),
32+
extends:
33+
(await findCodePushupEslintrc(project)) ?? getEslintConfig(project),
34+
})),
35+
),
36+
};
37+
38+
// include patterns from each project
39+
const patterns = projects.flatMap(project => [
40+
...getLintFilePatterns(project),
41+
// HACK: ESLint.calculateConfigForFile won't find rules included only for subsets of *.ts when globs used
42+
// so we explicitly provide additional patterns used by @code-pushup/eslint-config to ensure those rules are included
43+
// this workaround won't be necessary once flat configs are stable (much easier to find all rules)
44+
`${project.sourceRoot}/*.spec.ts`, // jest/* and vitest/* rules
45+
`${project.sourceRoot}/*.cy.ts`, // cypress/* rules
46+
`${project.sourceRoot}/*.stories.ts`, // storybook/* rules
47+
`${project.sourceRoot}/.storybook/main.ts`, // storybook/no-uninstalled-addons rule
48+
]);
49+
50+
return {
51+
eslintrc: eslintConfig,
52+
patterns,
53+
};
54+
}

0 commit comments

Comments
 (0)