-
Notifications
You must be signed in to change notification settings - Fork 16
Code PushUp integration guide for Nx monorepos
This is a guide for how to integrate Code PushUp CLI and ESLint plugin in an Nx monorepo, and how to automatically upload reports to portal's staging environment.
Warning
Only Nx 17 is supported. If your repo uses an older version, you'll need to update first - run npx nx migrate latest --interactive (confirm latest versions of TypeScript and Angular), followed by npx nx migrate --run-migrations (more info in Nx docs).
Code PushUp provides several recommended ESLint presets in the @code-pushup/eslint-config NPM package. It's a quick way of setting up a strict ESLint configuration, which can report a large amount of potential problems or code style suggestions in any codebase. The intention isn't to fix all the issues right away, but rather to start tracking them with Code PushUp.
Tip
The configuration and setup will differ dependending on your tech stack. One example is given below, but refer to the official @code-pushup/eslint-config what other configs are available and how to set them up.
Note that you can either extend a config for your entire monorepo in the root .eslintrc.json, or only extend it in a specific project's .eslintrc.json instead. This may be useful when your monorepo is more diverse, e.g. only front-end projects would extend @code-pushup/eslint-config/angular, but a back-end project would extend @code-pushup/eslint-config/node instead, while @code-pushup/eslint-config/typescript would be extended globally.
Example for Nx monorepo using Angular, Jest and Cypress
-
Install peer dependencies as required (for more info, see setup docs):
npm i -D eslint-plugin-{deprecation,functional@latest,import,no-secrets,promise,rxjs,sonarjs,unicorn@48} -
Install Code PushUp's ESLint config package:
npm i -D @code-pushup/eslint-config
-
In
.eslintrc.json, extend configs: -
Set
parserOptions.projectto correct tsconfig location in each Nx project's.eslintrc.json(more info in Nx docs). E.g.:{ "extends": ["../../.eslintrc.json"], "ignorePatterns": ["!**/*"], "overrides": [ { "files": ["*.ts", "*.tsx"], "parserOptions": { "project": ["libs/utils/tsconfig.*?.json"] } } ] } -
Test with
npx nx run-many -t lintornpx nx lint <project>to see what errors and warnings are reported. You can customize or even disable rules using therulessection in.eslintrc.json, if you need to tweak the configuration to better match your team's preferences, or even report an issue incode-pushup/eslint-configrepo.
At this point, you probably have a lot of problems being reported. If nx lint is required check in CI, some way to temporarily disable the failing rules is needed. While the CI should pass, we still want those problems to reported to Code PushUp.
This can be achieved by renaming each project's .eslintrc.json to code-pushup.eslintrc.json, and creating a new .eslintrc.json per project which extends ./code-pushup.eslintrc.json with additional overrides which turn off failing rules. You can copy-paste and run the eslint-to-code-pushup.mjs script (also pasted below) to automate this for you. The result should look like packages/core/.eslintrc.json from the Code PushUp CLI repo, for example.
eslint-to-code-pushup.mjs
import {
createProjectGraphAsync,
readProjectsConfigurationFromProjectGraph,
} from '@nx/devkit';
import { ESLint } from 'eslint';
import minimatch from 'minimatch';
import fs from 'node:fs/promises';
import path from 'node:path';
// replace these patterns as needed
const TEST_FILE_PATTERNS = [
'*.spec.ts',
'*.test.ts',
'**/test/**/*',
'**/mock/**/*',
'**/mocks/**/*',
'*.cy.ts',
'*.stories.ts',
];
const graph = await createProjectGraphAsync({ exitOnError: true });
const projects = Object.values(
readProjectsConfigurationFromProjectGraph(graph).projects,
)
.filter(project => 'lint' in (project.targets ?? {}))
.sort((a, b) => a.root.localeCompare(b.root));
for (let i = 0; i < projects.length; i++) {
const project = projects[i];
/** @type {import('@nx/eslint/src/executors/lint/schema').Schema} */
const options = project.targets.lint.options;
const eslintrc = options.eslintConfig ?? `${project.root}/.eslintrc.json`;
const patterns = options.lintFilePatterns;
console.info(
`Processing Nx ${project.projectType ?? 'project'} "${project.name}" (${
i + 1
}/${projects.length}) ...`,
);
const eslint = new ESLint({
overrideConfigFile: eslintrc,
useEslintrc: false,
errorOnUnmatchedPattern: false,
resolvePluginsRelativeTo: options.resolvePluginsRelativeTo ?? undefined,
ignorePath: options.ignorePath ?? undefined,
rulePaths: options.rulesdir ?? [],
});
const results = await eslint.lintFiles(patterns);
/** @type {Set<string>} */
const failingRules = new Set();
/** @type {Set<string>} */
const failingRulesTestsOnly = new Set();
/** @type {Map<string, number>} */
const errorCounts = new Map();
/** @type {Map<string, number>} */
const warningCounts = new Map();
for (const result of results) {
const isTestFile = TEST_FILE_PATTERNS.some(pattern =>
minimatch(result.filePath, pattern),
);
for (const { ruleId, severity } of result.messages) {
if (isTestFile) {
if (!failingRules.has(ruleId)) {
failingRulesTestsOnly.add(ruleId);
}
} else {
failingRules.add(ruleId);
failingRulesTestsOnly.delete(ruleId);
}
if (severity === 1) {
warningCounts.set(ruleId, (warningCounts.get(ruleId) ?? 0) + 1);
} else {
errorCounts.set(ruleId, (errorCounts.get(ruleId) ?? 0) + 1);
}
}
}
/** @param {string} ruleId */
const formatCounts = ruleId =>
[
{ kind: 'error', count: errorCounts.get(ruleId) },
{ kind: 'warning', count: warningCounts.get(ruleId) },
]
.filter(({ count }) => count > 0)
.map(({ kind, count }) =>
count === 1 ? `1 ${kind}` : `${count} ${kind}s`,
)
.join(', ');
if (failingRules.size > 0) {
console.info(`• ${failingRules.size} rules need to be disabled`);
failingRules.forEach(ruleId => {
console.info(` - ${ruleId} (${formatCounts(ruleId)})`);
});
}
if (failingRulesTestsOnly.size > 0) {
console.info(
`• ${failingRulesTestsOnly.size} rules need to be disabled only for test files`,
);
failingRulesTestsOnly.forEach(ruleId => {
console.info(` - ${ruleId} (${formatCounts(ruleId)})`);
});
}
if (failingRules.size === 0 && failingRulesTestsOnly.size === 0) {
console.info('• no rules need to be disabled, nothing to do here\n');
continue;
}
const cpEslintrc =
'code-pushup.' + path.basename(eslintrc).replace(/^\./, '');
/** @param {Set<string>} rules */
const formatRules = (rules, indentLevel = 2) =>
Array.from(rules.values())
.sort((a, b) => {
if (a.includes('/') !== b.includes('/')) {
return a.includes('/') ? 1 : -1;
}
return a.localeCompare(b);
})
.map(
(ruleId, i, arr) =>
' '.repeat(indentLevel) +
`"${ruleId}": "off"${
i === arr.length - 1 ? '' : ','
} // ${formatCounts(ruleId)}`,
)
.join('\n')
.replace(/,$/, '');
/** @type {import('eslint').Linter.Config} */
const config = `{
"extends": ["./${cpEslintrc}"],
// temporarily disable failing rules so \`nx lint\` passes
// number of errors/warnings per rule recorded at ${new Date().toString()}
"rules": {
${formatRules(failingRules)}
}
${
!failingRulesTestsOnly.size
? ''
: `,
"overrides": [
{
"files": ${JSON.stringify(TEST_FILE_PATTERNS)},
"rules": {
${formatRules(failingRulesTestsOnly, 4)}
}
}
]`
}
}`;
const content = /\.c?[jt]s$/.test(eslintrc)
? `module.exports = ${config}`
: config;
const cpEslintrcPath = path.join(project.root, cpEslintrc);
await fs.copyFile(eslintrc, cpEslintrcPath);
console.info(`• copied ${eslintrc} to ${cpEslintrcPath}`);
await fs.writeFile(eslintrc, content);
console.info(
`• replaced ${eslintrc} to extend ${cpEslintrc} and disable failing rules\n`,
);
}
process.exit(0);Verify that nx lint now passes for all your projects.
{ "root": true, "ignorePatterns": ["**/*"], "plugins": ["@nx"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "rules": { "@nx/enforce-module-boundaries": [ "error", { "enforceBuildableLibDependency": true, "allow": [], "depConstraints": [ { "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] } ] } ] } }, { "files": ["*.ts", "*.tsx"], "extends": [ "plugin:@nx/typescript", // extend configs for TS files "@code-pushup/eslint-config/angular", "@code-pushup/eslint-config/jest", "@code-pushup/eslint-config/cypress" ], "settings": { // configure TS path aliases "import/resolver": { "typescript": { "project": "tsconfig.base.json" } } }, "rules": { // ... customize as needed ... "@angular-eslint/component-selector": [ "warn", { "type": "element", "style": "kebab-case", "prefix": ["cp"] // <-- replace with your own prefix } ], "@angular-eslint/directive-selector": [ "warn", { "type": "attribute", "style": "camelCase", "prefix": "cp" // <-- replace with your own prefix } ], "@angular-eslint/pipe-prefix": [ "warn", { "prefixes": ["cp"] // <-- replace with your own prefix } ] } }, { "files": ["*.js", "*.jsx"], "extends": ["plugin:@nx/javascript", "@code-pushup"], // add default config for JS files "rules": {} } ] }