Skip to content

Commit 82fa3e1

Browse files
committed
feat(plugin-eslint): provide Nx helper to combine eslint config from all projects
1 parent 9bda262 commit 82fa3e1

File tree

6 files changed

+139
-40
lines changed

6 files changed

+139
-40
lines changed

packages/plugin-eslint/src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ import { eslintPlugin } from './lib/eslint-plugin';
33
export default eslintPlugin;
44

55
export type { ESLintPluginConfig } from './lib/config';
6+
7+
export { eslintConfigFromNxProjects } from './lib/nx';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {
2+
createProjectGraphAsync,
3+
readProjectsConfigurationFromProjectGraph,
4+
} from '@nx/devkit';
5+
import type { ESLint } from 'eslint';
6+
import type { ESLintPluginConfig } from '../config';
7+
import {
8+
findCodePushupEslintrc,
9+
getEslintConfig,
10+
getLintFilePatterns,
11+
} from './utils';
12+
13+
export async function eslintConfigFromNxProjects(): Promise<ESLintPluginConfig> {
14+
// find Nx projects with lint target
15+
const projectGraph = await createProjectGraphAsync({ exitOnError: false });
16+
const projectsConfiguration =
17+
readProjectsConfigurationFromProjectGraph(projectGraph);
18+
const projects = Object.values(projectsConfiguration.projects).filter(
19+
project => 'lint' in (project.targets ?? {}),
20+
);
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}/*.test.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+
};
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { eslintConfigFromNxProjects } from './find-all-projects';
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { ProjectConfiguration } from '@nx/devkit';
2+
import { fileExists, toArray } from '@code-pushup/utils';
3+
4+
export async function findCodePushupEslintrc(
5+
project: ProjectConfiguration,
6+
): Promise<string | null> {
7+
const name = 'code-pushup.eslintrc';
8+
// https://eslint.org/docs/latest/use/configure/configuration-files#configuration-file-formats
9+
const extensions = ['json', 'js', 'cjs', 'yml', 'yaml'];
10+
11+
// eslint-disable-next-line functional/no-loop-statements
12+
for (const ext of extensions) {
13+
const filename = `./${project.root}/${name}.${ext}`;
14+
if (await fileExists(filename)) {
15+
return filename;
16+
}
17+
}
18+
19+
return null;
20+
}
21+
22+
export function getLintFilePatterns(project: ProjectConfiguration): string[] {
23+
const options = project.targets?.['lint']?.options as
24+
| { lintFilePatterns?: string | string[] }
25+
| undefined;
26+
return options?.lintFilePatterns == null
27+
? []
28+
: toArray(options.lintFilePatterns);
29+
}
30+
31+
export function getEslintConfig(project: ProjectConfiguration): string {
32+
const options = project.targets?.['lint']?.options as
33+
| { eslintConfig?: string }
34+
| undefined;
35+
return options?.eslintConfig ?? `${project.root}/.eslintrc.json`;
36+
}

packages/utils/src/index.ts

+31-30
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,34 @@ export {
77
executeProcess,
88
objectToCliArgs,
99
} from './lib/execute-process';
10+
export {
11+
FileResult,
12+
MultipleFileResults,
13+
crawlFileSystem,
14+
ensureDirectoryExists,
15+
fileExists,
16+
findLineNumberInText,
17+
importEsmModule,
18+
logMultipleFileResults,
19+
pluginWorkDir,
20+
readJsonFile,
21+
readTextFile,
22+
toUnixPath,
23+
} from './lib/file-system';
24+
export {
25+
formatBytes,
26+
formatDuration,
27+
pluralize,
28+
pluralizeToken,
29+
slugify,
30+
} from './lib/formatting';
1031
export { getLatestCommit, git } from './lib/git';
32+
export {
33+
isPromiseFulfilledResult,
34+
isPromiseRejectedResult,
35+
} from './lib/guards';
36+
export { logMultipleResults } from './lib/log-results';
37+
export { NEW_LINE } from './lib/md';
1138
export { ProgressBar, getProgressBar } from './lib/progress';
1239
export {
1340
CODE_PUSHUP_DOMAIN,
@@ -21,37 +48,11 @@ export { reportToMd } from './lib/report-to-md';
2148
export { reportToStdout } from './lib/report-to-stdout';
2249
export { ScoredReport, scoreReport } from './lib/scoring';
2350
export {
24-
readJsonFile,
25-
readTextFile,
26-
toUnixPath,
27-
ensureDirectoryExists,
28-
FileResult,
29-
MultipleFileResults,
30-
logMultipleFileResults,
31-
importEsmModule,
32-
pluginWorkDir,
33-
crawlFileSystem,
34-
findLineNumberInText,
35-
} from './lib/file-system';
36-
export { verboseUtils } from './lib/verbose-utils';
37-
export {
38-
toArray,
39-
objectToKeys,
40-
objectToEntries,
4151
countOccurrences,
4252
distinct,
4353
factorOf,
54+
objectToEntries,
55+
objectToKeys,
56+
toArray,
4457
} from './lib/transformation';
45-
export {
46-
slugify,
47-
pluralize,
48-
pluralizeToken,
49-
formatBytes,
50-
formatDuration,
51-
} from './lib/formatting';
52-
export { NEW_LINE } from './lib/md';
53-
export { logMultipleResults } from './lib/log-results';
54-
export {
55-
isPromiseFulfilledResult,
56-
isPromiseRejectedResult,
57-
} from './lib/guards';
58+
export { verboseUtils } from './lib/verbose-utils';

packages/utils/src/lib/file-system.ts

+19-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,25 @@ import { join } from 'path';
55
import { formatBytes } from './formatting';
66
import { logMultipleResults } from './log-results';
77

8+
export async function readTextFile(path: string): Promise<string> {
9+
const buffer = await readFile(path);
10+
return buffer.toString();
11+
}
12+
13+
export async function readJsonFile<T = unknown>(path: string): Promise<T> {
14+
const text = await readTextFile(path);
15+
return JSON.parse(text);
16+
}
17+
18+
export async function fileExists(path: string): Promise<boolean> {
19+
try {
20+
const stats = await stat(path);
21+
return stats.isFile();
22+
} catch {
23+
return false;
24+
}
25+
}
26+
827
export function toUnixPath(
928
path: string,
1029
options?: { toRelative?: boolean },
@@ -30,16 +49,6 @@ export async function ensureDirectoryExists(baseDir: string) {
3049
}
3150
}
3251

33-
export async function readTextFile(path: string): Promise<string> {
34-
const buffer = await readFile(path);
35-
return buffer.toString();
36-
}
37-
38-
export async function readJsonFile<T = unknown>(path: string): Promise<T> {
39-
const text = await readTextFile(path);
40-
return JSON.parse(text);
41-
}
42-
4352
export type FileResult = readonly [string] | readonly [string, number];
4453
export type MultipleFileResults = PromiseSettledResult<FileResult>[];
4554

0 commit comments

Comments
 (0)