Skip to content

Commit effd5d2

Browse files
committed
feat(plugin-eslint): support new config format in nx helpers
1 parent 13579f3 commit effd5d2

File tree

6 files changed

+507
-144
lines changed

6 files changed

+507
-144
lines changed

packages/plugin-eslint/src/lib/meta/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { groupsFromRuleCategories, groupsFromRuleTypes } from './groups';
44
import { listRules } from './rules';
55
import { ruleToAudit } from './transform';
66

7+
export { detectConfigVersion, type ConfigFormat } from './versions';
8+
79
export async function listAuditsAndGroups(
810
targets: ESLintTarget[],
911
): Promise<{ audits: Audit[]; groups: Group[] }> {

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

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,21 @@ describe('Nx helpers', () => {
4646
patterns: [
4747
'packages/cli/**/*.ts',
4848
'packages/cli/package.json',
49-
'packages/cli/src/*.spec.ts',
50-
'packages/cli/src/*.cy.ts',
51-
'packages/cli/src/*.stories.ts',
52-
'packages/cli/src/.storybook/main.ts',
49+
'packages/cli/*.spec.ts',
50+
'packages/cli/*.cy.ts',
51+
'packages/cli/*.stories.ts',
52+
'packages/cli/.storybook/main.ts',
5353
],
5454
},
5555
{
5656
eslintrc: './packages/core/.eslintrc.json',
5757
patterns: [
5858
'packages/core/**/*.ts',
5959
'packages/core/package.json',
60-
'packages/core/src/*.spec.ts',
61-
'packages/core/src/*.cy.ts',
62-
'packages/core/src/*.stories.ts',
63-
'packages/core/src/.storybook/main.ts',
60+
'packages/core/*.spec.ts',
61+
'packages/core/*.cy.ts',
62+
'packages/core/*.stories.ts',
63+
'packages/core/.storybook/main.ts',
6464
],
6565
},
6666
{
@@ -69,21 +69,21 @@ describe('Nx helpers', () => {
6969
'packages/nx-plugin/**/*.ts',
7070
'packages/nx-plugin/package.json',
7171
'packages/nx-plugin/generators.json',
72-
'packages/nx-plugin/src/*.spec.ts',
73-
'packages/nx-plugin/src/*.cy.ts',
74-
'packages/nx-plugin/src/*.stories.ts',
75-
'packages/nx-plugin/src/.storybook/main.ts',
72+
'packages/nx-plugin/*.spec.ts',
73+
'packages/nx-plugin/*.cy.ts',
74+
'packages/nx-plugin/*.stories.ts',
75+
'packages/nx-plugin/.storybook/main.ts',
7676
],
7777
},
7878
{
7979
eslintrc: './packages/utils/.eslintrc.json',
8080
patterns: [
8181
'packages/utils/**/*.ts',
8282
'packages/utils/package.json',
83-
'packages/utils/src/*.spec.ts',
84-
'packages/utils/src/*.cy.ts',
85-
'packages/utils/src/*.stories.ts',
86-
'packages/utils/src/.storybook/main.ts',
83+
'packages/utils/*.spec.ts',
84+
'packages/utils/*.cy.ts',
85+
'packages/utils/*.stories.ts',
86+
'packages/utils/.storybook/main.ts',
8787
],
8888
},
8989
] satisfies ESLintTarget[]);
@@ -99,10 +99,10 @@ describe('Nx helpers', () => {
9999
'packages/nx-plugin/**/*.ts',
100100
'packages/nx-plugin/package.json',
101101
'packages/nx-plugin/generators.json',
102-
'packages/nx-plugin/src/*.spec.ts',
103-
'packages/nx-plugin/src/*.cy.ts',
104-
'packages/nx-plugin/src/*.stories.ts',
105-
'packages/nx-plugin/src/.storybook/main.ts',
102+
'packages/nx-plugin/*.spec.ts',
103+
'packages/nx-plugin/*.cy.ts',
104+
'packages/nx-plugin/*.stories.ts',
105+
'packages/nx-plugin/.storybook/main.ts',
106106
],
107107
},
108108
] satisfies ESLintTarget[]);

packages/plugin-eslint/src/lib/nx/projects-to-config.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { ProjectConfiguration, ProjectGraph } from '@nx/devkit';
22
import type { ESLintTarget } from '../config';
3+
import { detectConfigVersion } from '../meta';
34
import {
4-
findCodePushupEslintrc,
5-
getEslintConfig,
5+
findCodePushupEslintConfig,
6+
findEslintConfig,
67
getLintFilePatterns,
78
} from './utils';
89

@@ -21,21 +22,15 @@ export async function nxProjectsToConfig(
2122
.filter(predicate) // apply predicate
2223
.sort((a, b) => a.root.localeCompare(b.root));
2324

25+
const format = await detectConfigVersion();
26+
2427
return Promise.all(
2528
projects.map(
2629
async (project): Promise<ESLintTarget> => ({
2730
eslintrc:
28-
(await findCodePushupEslintrc(project)) ?? getEslintConfig(project),
29-
patterns: [
30-
...getLintFilePatterns(project),
31-
// HACK: ESLint.calculateConfigForFile won't find rules included only for subsets of *.ts when globs used
32-
// so we explicitly provide additional patterns used by @code-pushup/eslint-config to ensure those rules are included
33-
// this workaround won't be necessary once flat configs are stable (much easier to find all rules)
34-
`${project.sourceRoot}/*.spec.ts`, // jest/* and vitest/* rules
35-
`${project.sourceRoot}/*.cy.ts`, // cypress/* rules
36-
`${project.sourceRoot}/*.stories.ts`, // storybook/* rules
37-
`${project.sourceRoot}/.storybook/main.ts`, // storybook/no-uninstalled-addons rule
38-
],
31+
(await findCodePushupEslintConfig(project, format)) ??
32+
(await findEslintConfig(project, format)),
33+
patterns: getLintFilePatterns(project, format),
3934
}),
4035
),
4136
);

packages/plugin-eslint/src/lib/nx/projects-to-config.unit.test.ts

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,9 @@ describe('nxProjectsToConfig', () => {
2525
const config = await nxProjectsToConfig(projectGraph);
2626

2727
expect(config).toEqual<ESLintPluginConfig>([
28-
{
29-
eslintrc: './apps/client/.eslintrc.json',
30-
patterns: expect.arrayContaining(['apps/client/**/*.ts']),
31-
},
32-
{
33-
eslintrc: './apps/server/.eslintrc.json',
34-
patterns: expect.arrayContaining(['apps/server/**/*.ts']),
35-
},
36-
{
37-
eslintrc: './libs/models/.eslintrc.json',
38-
patterns: expect.arrayContaining(['libs/models/**/*.ts']),
39-
},
28+
{ patterns: expect.arrayContaining(['apps/client/**/*.ts']) },
29+
{ patterns: expect.arrayContaining(['apps/server/**/*.ts']) },
30+
{ patterns: expect.arrayContaining(['libs/models/**/*.ts']) },
4031
]);
4132
});
4233

@@ -65,10 +56,7 @@ describe('nxProjectsToConfig', () => {
6556
);
6657

6758
expect(config).toEqual<ESLintPluginConfig>([
68-
{
69-
eslintrc: './libs/models/.eslintrc.json',
70-
patterns: expect.arrayContaining(['libs/models/**/*.ts']),
71-
},
59+
{ patterns: expect.arrayContaining(['libs/models/**/*.ts']) },
7260
]);
7361
});
7462

@@ -107,18 +95,13 @@ describe('nxProjectsToConfig', () => {
10795
const config = await nxProjectsToConfig(projectGraph);
10896

10997
expect(config).toEqual<ESLintPluginConfig>([
110-
{
111-
eslintrc: './apps/client/.eslintrc.json',
112-
patterns: expect.arrayContaining(['apps/client/**/*.ts']),
113-
},
114-
{
115-
eslintrc: './apps/server/.eslintrc.json',
116-
patterns: expect.arrayContaining(['apps/server/**/*.ts']),
117-
},
98+
{ patterns: expect.arrayContaining(['apps/client/**/*.ts']) },
99+
{ patterns: expect.arrayContaining(['apps/server/**/*.ts']) },
118100
]);
119101
});
120102

121-
it('should use code-pushup.eslintrc.json if available', async () => {
103+
it('should use code-pushup.eslintrc.json if available and using legacy config', async () => {
104+
vi.stubEnv('ESLINT_USE_FLAT_CONFIG', 'false');
122105
vol.fromJSON(
123106
{
124107
'apps/client/code-pushup.eslintrc.json':
@@ -139,6 +122,45 @@ describe('nxProjectsToConfig', () => {
139122
]);
140123
});
141124

125+
it('should use eslint.strict.config.js if available and using flat config', async () => {
126+
vi.stubEnv('ESLINT_USE_FLAT_CONFIG', 'true');
127+
vol.fromJSON(
128+
{
129+
'apps/client/eslint.strict.config.js': 'export default [/*...*/]',
130+
},
131+
MEMFS_VOLUME,
132+
);
133+
const projectGraph = toProjectGraph([
134+
{ name: 'client', type: 'app', data: { root: 'apps/client' } },
135+
]);
136+
137+
const config = await nxProjectsToConfig(projectGraph);
138+
139+
expect(config).toEqual([
140+
expect.objectContaining<Partial<ESLintTarget>>({
141+
eslintrc: './apps/client/eslint.strict.config.js',
142+
}),
143+
]);
144+
});
145+
146+
it('should NOT use code-pushup.eslintrc.json if available but using flat config', async () => {
147+
vi.stubEnv('ESLINT_USE_FLAT_CONFIG', 'true');
148+
vol.fromJSON(
149+
{
150+
'apps/client/code-pushup.eslintrc.json':
151+
'{ "eslintrc": "@code-pushup" }',
152+
},
153+
MEMFS_VOLUME,
154+
);
155+
const projectGraph = toProjectGraph([
156+
{ name: 'client', type: 'app', data: { root: 'apps/client' } },
157+
]);
158+
159+
const config = await nxProjectsToConfig(projectGraph);
160+
161+
expect(config[0]!.eslintrc).toBeUndefined();
162+
});
163+
142164
it("should use each project's lint file patterns", async () => {
143165
const projectGraph = toProjectGraph([
144166
{
@@ -176,14 +198,12 @@ describe('nxProjectsToConfig', () => {
176198

177199
await expect(nxProjectsToConfig(projectGraph)).resolves.toEqual([
178200
{
179-
eslintrc: './apps/client/.eslintrc.json',
180201
patterns: expect.arrayContaining([
181202
'apps/client/**/*.ts',
182203
'apps/client/**/*.html',
183204
]),
184205
},
185206
{
186-
eslintrc: './apps/server/.eslintrc.json',
187207
patterns: expect.arrayContaining(['apps/server/**/*.ts']),
188208
},
189209
] satisfies ESLintPluginConfig);
Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,105 @@
11
import type { ProjectConfiguration } from '@nx/devkit';
22
import { join } from 'node:path';
33
import { fileExists, toArray } from '@code-pushup/utils';
4+
import type { ConfigFormat } from '../meta';
45

5-
export async function findCodePushupEslintrc(
6-
project: ProjectConfiguration,
7-
): Promise<string | null> {
8-
const name = 'code-pushup.eslintrc';
6+
const ESLINT_CONFIG_EXTENSIONS: Record<ConfigFormat, string[]> = {
7+
// https://eslint.org/docs/latest/use/configure/configuration-files#configuration-file-formats
8+
flat: ['js', 'mjs', 'cjs'],
9+
// https://eslint.org/docs/latest/use/configure/configuration-files-deprecated
10+
legacy: ['json', 'js', 'cjs', 'yml', 'yaml'],
11+
};
12+
const ESLINT_CONFIG_NAMES: Record<ConfigFormat, string[]> = {
913
// https://eslint.org/docs/latest/use/configure/configuration-files#configuration-file-formats
10-
const extensions = ['json', 'js', 'cjs', 'yml', 'yaml'];
14+
flat: ['eslint.config'],
15+
// https://eslint.org/docs/latest/use/configure/configuration-files-deprecated
16+
legacy: ['.eslintrc'],
17+
};
1118

12-
// eslint-disable-next-line functional/no-loop-statements
13-
for (const ext of extensions) {
14-
const filename = `./${project.root}/${name}.${ext}`;
15-
if (await fileExists(join(process.cwd(), filename))) {
16-
return filename;
17-
}
18-
}
19+
const CP_ESLINT_CONFIG_NAMES: Record<ConfigFormat, string[]> = {
20+
flat: [
21+
'code-pushup.eslint.config',
22+
'eslint.code-pushup.config',
23+
'eslint.config.code-pushup',
24+
'eslint.strict.config',
25+
'eslint.config.strict',
26+
],
27+
legacy: ['code-pushup.eslintrc', '.eslintrc.code-pushup', '.eslintrc.strict'],
28+
};
1929

20-
return null;
30+
export async function findCodePushupEslintConfig(
31+
project: ProjectConfiguration,
32+
format: ConfigFormat,
33+
): Promise<string | undefined> {
34+
return findProjectFile(project, {
35+
names: CP_ESLINT_CONFIG_NAMES[format],
36+
extensions: ESLINT_CONFIG_EXTENSIONS[format],
37+
});
2138
}
2239

23-
export function getLintFilePatterns(project: ProjectConfiguration): string[] {
40+
export async function findEslintConfig(
41+
project: ProjectConfiguration,
42+
format: ConfigFormat,
43+
): Promise<string | undefined> {
2444
const options = project.targets?.['lint']?.options as
25-
| { lintFilePatterns?: string | string[] }
45+
| { eslintConfig?: string }
2646
| undefined;
27-
return options?.lintFilePatterns == null
28-
? [`${project.root}/**/*`] // lintFilePatterns defaults to ["{projectRoot}"] - https://github.com/nrwl/nx/pull/20313
29-
: toArray(options.lintFilePatterns);
47+
return (
48+
options?.eslintConfig ??
49+
(await findProjectFile(project, {
50+
names: ESLINT_CONFIG_NAMES[format],
51+
extensions: ESLINT_CONFIG_EXTENSIONS[format],
52+
}))
53+
);
3054
}
3155

32-
export function getEslintConfig(
56+
export function getLintFilePatterns(
3357
project: ProjectConfiguration,
34-
): string | undefined {
58+
format: ConfigFormat,
59+
): string[] {
3560
const options = project.targets?.['lint']?.options as
36-
| { eslintConfig?: string }
61+
| { lintFilePatterns?: string | string[] }
3762
| undefined;
38-
return options?.eslintConfig ?? `./${project.root}/.eslintrc.json`;
63+
// lintFilePatterns defaults to ["{projectRoot}"] - https://github.com/nrwl/nx/pull/20313
64+
const defaultPatterns =
65+
format === 'legacy'
66+
? `${project.root}/**/*` // files not folder needed for legacy because rules detected with ESLint.calculateConfigForFile
67+
: project.root;
68+
const patterns =
69+
options?.lintFilePatterns == null
70+
? [defaultPatterns]
71+
: toArray(options.lintFilePatterns);
72+
if (format === 'legacy') {
73+
return [
74+
...patterns,
75+
// HACK: ESLint.calculateConfigForFile won't find rules included only for subsets of *.ts when globs used
76+
// so we explicitly provide additional patterns used by @code-pushup/eslint-config to ensure those rules are included
77+
// this workaround is only necessary for legacy configs (rules are detected more reliably in flat configs)
78+
`${project.root}/*.spec.ts`, // jest/* and vitest/* rules
79+
`${project.root}/*.cy.ts`, // cypress/* rules
80+
`${project.root}/*.stories.ts`, // storybook/* rules
81+
`${project.root}/.storybook/main.ts`, // storybook/no-uninstalled-addons rule
82+
];
83+
}
84+
return patterns;
85+
}
86+
87+
async function findProjectFile(
88+
project: ProjectConfiguration,
89+
file: {
90+
names: string[];
91+
extensions: string[];
92+
},
93+
): Promise<string | undefined> {
94+
// eslint-disable-next-line functional/no-loop-statements
95+
for (const name of file.names) {
96+
// eslint-disable-next-line functional/no-loop-statements
97+
for (const ext of file.extensions) {
98+
const filename = `./${project.root}/${name}.${ext}`;
99+
if (await fileExists(join(process.cwd(), filename))) {
100+
return filename;
101+
}
102+
}
103+
}
104+
return undefined;
39105
}

0 commit comments

Comments
 (0)