diff --git a/e2e/plugin-eslint-e2e/mocks/fixtures/artifacts-config/code-pushup.config.ts b/e2e/plugin-eslint-e2e/mocks/fixtures/artifacts-config/code-pushup.config.ts new file mode 100644 index 000000000..3483337ec --- /dev/null +++ b/e2e/plugin-eslint-e2e/mocks/fixtures/artifacts-config/code-pushup.config.ts @@ -0,0 +1,16 @@ +import eslintPlugin from '@code-pushup/eslint-plugin'; + +export default { + plugins: [ + await eslintPlugin( + { patterns: ['src/*.js'] }, + { + artifacts: { + generateArtifactsCommand: + 'npx eslint src/*.js --format json --output-file ./.code-pushup/eslint-report.json', + artifactsPaths: ['./.code-pushup/eslint-report.json'], + }, + }, + ), + ], +}; diff --git a/e2e/plugin-eslint-e2e/mocks/fixtures/artifacts-config/eslint.config.cjs b/e2e/plugin-eslint-e2e/mocks/fixtures/artifacts-config/eslint.config.cjs new file mode 100644 index 000000000..cef1161f4 --- /dev/null +++ b/e2e/plugin-eslint-e2e/mocks/fixtures/artifacts-config/eslint.config.cjs @@ -0,0 +1,13 @@ +/** @type {import('eslint').Linter.Config[]} */ +module.exports = [ + { + ignores: ['code-pushup.config.ts'], + }, + { + rules: { + eqeqeq: 'error', + 'max-lines': ['warn', 100], + 'no-unused-vars': 'warn', + }, + }, +]; diff --git a/e2e/plugin-eslint-e2e/mocks/fixtures/artifacts-config/src/index.js b/e2e/plugin-eslint-e2e/mocks/fixtures/artifacts-config/src/index.js new file mode 100644 index 000000000..39665c6ec --- /dev/null +++ b/e2e/plugin-eslint-e2e/mocks/fixtures/artifacts-config/src/index.js @@ -0,0 +1,9 @@ +function unusedFn() { + return '42'; +} + +module.exports = function orwell() { + if (2 + 2 == 5) { + console.log(1984); + } +}; diff --git a/e2e/plugin-eslint-e2e/tests/__snapshots__/collect.e2e.test.ts.snap b/e2e/plugin-eslint-e2e/tests/__snapshots__/collect.e2e.test.ts.snap index b20625b3b..a3de821a2 100644 --- a/e2e/plugin-eslint-e2e/tests/__snapshots__/collect.e2e.test.ts.snap +++ b/e2e/plugin-eslint-e2e/tests/__snapshots__/collect.e2e.test.ts.snap @@ -225,3 +225,235 @@ exports[`PLUGIN collect report with eslint-plugin NPM package > should run ESLin ], } `; + +exports[`PLUGIN collect report with eslint-plugin NPM package > should run ESLint plugin with artifacts options 1`] = ` +{ + "packageName": "@code-pushup/core", + "plugins": [ + { + "audits": [ + { + "description": "ESLint rule **eqeqeq**.", + "details": { + "issues": [ + { + "message": "Expected '===' and instead saw '=='.", + "severity": "error", + "source": { + "file": "tmp/e2e/plugin-eslint-e2e/__test__/artifacts-config/src/index.js", + "position": { + "endColumn": 15, + "endLine": 6, + "startColumn": 13, + "startLine": 6, + }, + }, + }, + ], + }, + "displayValue": "1 error", + "docsUrl": "https://eslint.org/docs/latest/rules/eqeqeq", + "score": 0, + "slug": "eqeqeq", + "title": "Require the use of \`===\` and \`!==\`", + "value": 1, + }, + { + "description": "ESLint rule **max-lines**. + +Custom options: + +\`\`\`json +100 +\`\`\`", + "details": { + "issues": [], + }, + "displayValue": "passed", + "docsUrl": "https://eslint.org/docs/latest/rules/max-lines", + "score": 1, + "slug": "max-lines-71b54366cb01f77b", + "title": "Enforce a maximum number of lines per file", + "value": 0, + }, + { + "description": "ESLint rule **no-unused-vars**.", + "details": { + "issues": [ + { + "message": "'unusedFn' is defined but never used.", + "severity": "warning", + "source": { + "file": "tmp/e2e/plugin-eslint-e2e/__test__/artifacts-config/src/index.js", + "position": { + "endColumn": 18, + "endLine": 1, + "startColumn": 10, + "startLine": 1, + }, + }, + }, + ], + }, + "displayValue": "1 warning", + "docsUrl": "https://eslint.org/docs/latest/rules/no-unused-vars", + "score": 0, + "slug": "no-unused-vars", + "title": "Disallow unused variables", + "value": 1, + }, + ], + "description": "Official Code PushUp ESLint plugin", + "docsUrl": "https://www.npmjs.com/package/@code-pushup/eslint-plugin", + "groups": [ + { + "description": "Code that either will cause an error or may cause confusing behavior. Developers should consider this a high priority to resolve.", + "refs": [ + { + "slug": "no-unused-vars", + "weight": 1, + }, + ], + "slug": "problems", + "title": "Problems", + }, + { + "description": "Something that could be done in a better way but no errors will occur if the code isn't changed.", + "refs": [ + { + "slug": "eqeqeq", + "weight": 1, + }, + { + "slug": "max-lines-71b54366cb01f77b", + "weight": 1, + }, + ], + "slug": "suggestions", + "title": "Suggestions", + }, + ], + "icon": "eslint", + "packageName": "@code-pushup/eslint-plugin", + "slug": "eslint", + "title": "ESLint", + }, + ], +} +`; + +exports[`PLUGIN collect report with eslint-plugin NPM package > should run ESLint plugin with artifacts options and create eslint-report.json and report.json 1`] = ` +{ + "packageName": "@code-pushup/core", + "plugins": [ + { + "audits": [ + { + "description": "ESLint rule **eqeqeq**.", + "details": { + "issues": [ + { + "message": "Expected '===' and instead saw '=='.", + "severity": "error", + "source": { + "file": "tmp/e2e/plugin-eslint-e2e/__test__/artifacts-config/src/index.js", + "position": { + "endColumn": 15, + "endLine": 6, + "startColumn": 13, + "startLine": 6, + }, + }, + }, + ], + }, + "displayValue": "1 error", + "docsUrl": "https://eslint.org/docs/latest/rules/eqeqeq", + "score": 0, + "slug": "eqeqeq", + "title": "Require the use of \`===\` and \`!==\`", + "value": 1, + }, + { + "description": "ESLint rule **max-lines**. + +Custom options: + +\`\`\`json +100 +\`\`\`", + "details": { + "issues": [], + }, + "displayValue": "passed", + "docsUrl": "https://eslint.org/docs/latest/rules/max-lines", + "score": 1, + "slug": "max-lines-71b54366cb01f77b", + "title": "Enforce a maximum number of lines per file", + "value": 0, + }, + { + "description": "ESLint rule **no-unused-vars**.", + "details": { + "issues": [ + { + "message": "'unusedFn' is defined but never used.", + "severity": "warning", + "source": { + "file": "tmp/e2e/plugin-eslint-e2e/__test__/artifacts-config/src/index.js", + "position": { + "endColumn": 18, + "endLine": 1, + "startColumn": 10, + "startLine": 1, + }, + }, + }, + ], + }, + "displayValue": "1 warning", + "docsUrl": "https://eslint.org/docs/latest/rules/no-unused-vars", + "score": 0, + "slug": "no-unused-vars", + "title": "Disallow unused variables", + "value": 1, + }, + ], + "description": "Official Code PushUp ESLint plugin", + "docsUrl": "https://www.npmjs.com/package/@code-pushup/eslint-plugin", + "groups": [ + { + "description": "Code that either will cause an error or may cause confusing behavior. Developers should consider this a high priority to resolve.", + "refs": [ + { + "slug": "no-unused-vars", + "weight": 1, + }, + ], + "slug": "problems", + "title": "Problems", + }, + { + "description": "Something that could be done in a better way but no errors will occur if the code isn't changed.", + "refs": [ + { + "slug": "eqeqeq", + "weight": 1, + }, + { + "slug": "max-lines-71b54366cb01f77b", + "weight": 1, + }, + ], + "slug": "suggestions", + "title": "Suggestions", + }, + ], + "icon": "eslint", + "packageName": "@code-pushup/eslint-plugin", + "slug": "eslint", + "title": "ESLint", + }, + ], +} +`; diff --git a/e2e/plugin-eslint-e2e/tests/collect.e2e.test.ts b/e2e/plugin-eslint-e2e/tests/collect.e2e.test.ts index 98966c6b8..bbfada16a 100644 --- a/e2e/plugin-eslint-e2e/tests/collect.e2e.test.ts +++ b/e2e/plugin-eslint-e2e/tests/collect.e2e.test.ts @@ -20,6 +20,7 @@ describe('PLUGIN collect report with eslint-plugin NPM package', () => { ); const fixturesFlatConfigDir = path.join(fixturesDir, 'flat-config'); const fixturesLegacyConfigDir = path.join(fixturesDir, 'legacy-config'); + const fixturesArtifactsConfigDir = path.join(fixturesDir, 'artifacts-config'); const envRoot = path.join( E2E_ENVIRONMENTS_DIR, @@ -28,22 +29,32 @@ describe('PLUGIN collect report with eslint-plugin NPM package', () => { ); const flatConfigDir = path.join(envRoot, 'flat-config'); const legacyConfigDir = path.join(envRoot, 'legacy-config'); + const artifactsConfigDir = path.join(envRoot, 'artifacts-config'); const flatConfigOutputDir = path.join(flatConfigDir, '.code-pushup'); const legacyConfigOutputDir = path.join(legacyConfigDir, '.code-pushup'); + const artifactsConfigOutputDir = path.join( + artifactsConfigDir, + '.code-pushup', + ); beforeAll(async () => { await cp(fixturesFlatConfigDir, flatConfigDir, { recursive: true }); await cp(fixturesLegacyConfigDir, legacyConfigDir, { recursive: true }); + await cp(fixturesArtifactsConfigDir, artifactsConfigDir, { + recursive: true, + }); }); afterAll(async () => { await teardownTestFolder(flatConfigDir); await teardownTestFolder(legacyConfigDir); + await teardownTestFolder(artifactsConfigDir); }); afterEach(async () => { await teardownTestFolder(flatConfigOutputDir); await teardownTestFolder(legacyConfigOutputDir); + await teardownTestFolder(artifactsConfigOutputDir); }); it('should run ESLint plugin for flat config and create report.json', async () => { @@ -80,4 +91,21 @@ describe('PLUGIN collect report with eslint-plugin NPM package', () => { expect(() => reportSchema.parse(report)).not.toThrow(); expect(omitVariableReportData(report as Report)).toMatchSnapshot(); }); + + it('should run ESLint plugin with artifacts options and create eslint-report.json and report.json', async () => { + const { code } = await executeProcess({ + command: 'npx', + args: ['@code-pushup/cli', 'collect', '--no-progress'], + cwd: artifactsConfigDir, + }); + + expect(code).toBe(0); + + const report = await readJsonFile( + path.join(artifactsConfigOutputDir, 'report.json'), + ); + + expect(() => reportSchema.parse(report)).not.toThrow(); + expect(omitVariableReportData(report as Report)).toMatchSnapshot(); + }); }); diff --git a/nx.json b/nx.json index b612c5eed..5f3c675ac 100644 --- a/nx.json +++ b/nx.json @@ -185,7 +185,7 @@ }, "code-pushup-eslint": { "cache": true, - "inputs": ["default", "code-pushup-inputs", "lint-eslint-inputs"], + "inputs": ["default", "code-pushup-inputs"], "outputs": ["{projectRoot}/.code-pushup/eslint/runner-output.json"], "executor": "nx:run-commands", "options": { diff --git a/packages/plugin-eslint/README.md b/packages/plugin-eslint/README.md index 75a45dc63..a1ace02ff 100644 --- a/packages/plugin-eslint/README.md +++ b/packages/plugin-eslint/README.md @@ -226,6 +226,149 @@ export default { 2. Run the CLI with `npx code-pushup collect` and view or upload report (refer to [CLI docs](../cli/README.md)). +## Artifacts generation and loading + +In addition to running ESLint from the plugin implementation, you can configure the plugin to consume pre-generated ESLint reports (artifacts). This is particularly useful for: + +- **CI/CD pipelines**: Use cached lint results from your build system +- **Monorepo setups**: Aggregate results from multiple projects or targets +- **Performance optimization**: Skip ESLint execution when reports are already available +- **Custom workflows**: Integrate with existing linting infrastructure + +The artifacts feature supports loading ESLint JSON reports that follow the standard `ESLint.LintResult[]` format. + +### Basic artifact configuration + +Specify the path(s) to your ESLint JSON report files: + +```js +import eslintPlugin from '@code-pushup/eslint-plugin'; + +export default { + plugins: [ + await eslintPlugin({ + artifacts: { + artifactsPaths: './eslint-report.json', + }, + }), + ], +}; +``` + +### Multiple artifact files + +Use glob patterns to aggregate results from multiple files: + +```js +export default { + plugins: [ + await eslintPlugin({ + artifacts: { + artifactsPaths: ['packages/**/eslint-report.json', 'apps/**/.eslint/*.json'], + }, + }), + ], +}; +``` + +### Generate artifacts with custom command + +If you need to generate the artifacts before loading them, use the `generateArtifactsCommand` option: + +```js +export default { + plugins: [ + await eslintPlugin({ + artifacts: { + generateArtifactsCommand: 'npm run lint:report', + artifactsPaths: './eslint-report.json', + }, + }), + ], +}; +``` + +You can also specify the command with arguments: + +```js +export default { + plugins: [ + await eslintPlugin({ + artifacts: { + generateArtifactsCommand: { + command: 'eslint', + args: ['src/**/*.{js,ts}', '--format=json', '--output-file=eslint-report.json'], + }, + artifactsPaths: './eslint-report.json', + }, + }), + ], +}; +``` + ## Nx Monorepo Setup -Find all details in our [Nx setup guide](https://github.com/code-pushup/cli/wiki/Code-PushUp-integration-guide-for-Nx-monorepos#eslint-config). +### Caching artifact generation + +To leverage Nx's caching capabilities, you need to generate a JSON artifact for caching, while still being able to see the ESLint violations in the terminal or CI logs, so you can fix them. +This can be done by leveraging eslint formatter. + +_lint target from nx.json_ + +```json +{ + "lint": { + "inputs": ["lint-eslint-inputs"], + "outputs": ["{projectRoot}/.eslint/**/*"], + "cache": true, + "executor": "nx:run-commands", + "options": { + "command": "eslint", + "args": ["{projectRoot}/**/*.ts", "{projectRoot}/package.json", "--config={projectRoot}/eslint.config.js", "--max-warnings=0", "--no-warn-ignored", "--error-on-unmatched-pattern=false", "--format=@code-pushup/eslint-formatter-multi"], + "env": { + "ESLINT_FORMATTER_CONFIG": "{\"outputDir\":\"{projectRoot}/.eslint\"}" + } + } + } +} +``` + +As you can now generate the `eslint-report.json` from cache your plugin configuration can directly consume them. + +_code-pushup.config.ts target from nx.json_ + +```jsonc +{ + "code-pushup": { + "dependsOn": ["lint"], + // also multiple targets can be merged into one report + // "dependsOn": ["lint", "lint-next"], + "executor": "nx:run-commands", + "options": { + "command": "npx code-pushup", + }, + }, +} +``` + +and the project configuration leverages `dependsOn` to ensure the artefacts are generated when running code-pushup. + +Your `code-pushup.config.ts` can then be configured to consume the cached artifacts: + +```js +import eslintPlugin from '@code-pushup/eslint-plugin'; + +export default { + plugins: [ + await eslintPlugin({ + artifacts: { + artifactsPaths: 'packages/**/.eslint/eslint-report-*.json', + }, + }), + ], +}; +``` + +--- + +Find more details in our [Nx setup guide](https://github.com/code-pushup/cli/wiki/Code-PushUp-integration-guide-for-Nx-monorepos#eslint-config).