Skip to content

Commit 9a7e128

Browse files
chore: add eslint formatter for multiple formats (#1090)
This PR includes: - added MVP custom lint formatter under tools - add `lint-report` to `nx.json#defaultTargets` Task Graph: <img width="1324" height="327" alt="Screenshot 2025-09-02 at 13 51 18" src="https://github.com/user-attachments/assets/bf535954-a556-412b-a815-928d1c6aedca" /> > [!note] > This enables us to reuse the lint work. We always create json + terminal output and in the conformance target we just depend on the targets and consume the output. In the past we where running the lint work 2 times as it is not possible to get terminal for DX and json for conformance Related: - #1068 --------- Co-authored-by: Matěj Chalk <34691111+matejchalk@users.noreply.github.com>
1 parent aa4540a commit 9a7e128

21 files changed

+899
-37
lines changed

code-pushup.preset.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,13 @@ export const eslintCoreConfigNx = async (
177177
eslintrc: `packages/${projectName}/eslint.config.js`,
178178
patterns: ['.'],
179179
})
180-
: await eslintPlugin(await eslintConfigFromAllNxProjects()),
180+
: await eslintPlugin(await eslintConfigFromAllNxProjects(), {
181+
artifacts: {
182+
// We leverage Nx dependsOn to only run all lint targets before we run code-pushup
183+
// generateArtifactsCommand: 'npx nx run-many -t lint',
184+
artifactsPaths: ['packages/**/.eslint/eslint-report.json'],
185+
},
186+
}),
181187
],
182188
categories: eslintCategories,
183189
});

nx.json

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,40 @@
4949
}
5050
],
5151
"sharedGlobals": [
52-
{ "runtime": "node -e \"console.log(require('os').platform())\"" },
53-
{ "runtime": "node -v" },
54-
{ "runtime": "npm -v" }
52+
{
53+
"runtime": "node -e \"console.log(require('os').platform())\""
54+
},
55+
{
56+
"runtime": "node -v"
57+
},
58+
{
59+
"runtime": "npm -v"
60+
}
5561
]
5662
},
5763
"targetDefaults": {
64+
"lint": {
65+
"dependsOn": ["eslint-formatter-multi:build"],
66+
"inputs": ["lint-eslint-inputs"],
67+
"outputs": ["{projectRoot}/.eslint/**/*"],
68+
"cache": true,
69+
"executor": "nx:run-commands",
70+
"options": {
71+
"command": "eslint",
72+
"args": [
73+
"{projectRoot}/**/*.ts",
74+
"{projectRoot}/package.json",
75+
"--config={projectRoot}/eslint.config.js",
76+
"--max-warnings=0",
77+
"--no-warn-ignored",
78+
"--error-on-unmatched-pattern=false",
79+
"--format=./tools/eslint-formatter-multi/dist/src/index.js"
80+
],
81+
"env": {
82+
"ESLINT_FORMATTER_CONFIG": "{\"outputDir\":\"{projectRoot}/.eslint\"}"
83+
}
84+
}
85+
},
5886
"build": {
5987
"dependsOn": ["^build"],
6088
"inputs": ["production", "^production"],
@@ -97,36 +125,6 @@
97125
"inputs": ["default"],
98126
"cache": true
99127
},
100-
"lint": {
101-
"inputs": ["lint-eslint-inputs"],
102-
"executor": "@nx/eslint:lint",
103-
"outputs": ["{options.outputFile}"],
104-
"cache": true,
105-
"options": {
106-
"errorOnUnmatchedPattern": false,
107-
"maxWarnings": 0,
108-
"lintFilePatterns": [
109-
"{projectRoot}/**/*.ts",
110-
"{projectRoot}/package.json"
111-
]
112-
}
113-
},
114-
"lint-report": {
115-
"inputs": ["default", "{workspaceRoot}/eslint.config.?(c)js"],
116-
"outputs": ["{projectRoot}/.eslint/eslint-report*.json"],
117-
"cache": true,
118-
"executor": "@nx/linter:eslint",
119-
"options": {
120-
"errorOnUnmatchedPattern": false,
121-
"maxWarnings": 0,
122-
"format": "json",
123-
"outputFile": "{projectRoot}/.eslint/eslint-report.json",
124-
"lintFilePatterns": [
125-
"{projectRoot}/**/*.ts",
126-
"{projectRoot}/package.json"
127-
]
128-
}
129-
},
130128
"nxv-pkg-install": {
131129
"parallelism": false
132130
},

packages/plugin-eslint/src/lib/runner/lint.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ async function executeLint({
2525
patterns,
2626
}: ESLintTarget): Promise<ESLint.LintResult[]> {
2727
// running as CLI because ESLint#lintFiles() runs out of memory
28-
const { stdout } = await executeProcess({
28+
const { stdout, stderr, code } = await executeProcess({
2929
command: 'npx',
3030
args: [
3131
'eslint',
@@ -42,6 +42,12 @@ async function executeLint({
4242
cwd: process.cwd(),
4343
});
4444

45+
if (!stdout.trim()) {
46+
throw new Error(
47+
`ESLint produced empty output. Exit code: ${code}, STDERR: ${stderr}`,
48+
);
49+
}
50+
4551
return JSON.parse(stdout) as ESLint.LintResult[];
4652
}
4753

project.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@
1616
}
1717
]
1818
},
19-
"code-pushup-eslint": {},
19+
"code-pushup-eslint": {
20+
"dependsOn": [
21+
{
22+
"target": "lint",
23+
"projects": "*"
24+
}
25+
]
26+
},
2027
"code-pushup-jsdocs": {},
2128
"code-pushup-typescript": {},
2229
"code-pushup": {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# ESLint Multi Formatter
2+
3+
The ESLint plugin uses a custom formatter that supports multiple output formats and destinations simultaneously.
4+
5+
## Configuration
6+
7+
Use the `ESLINT_FORMATTER_CONFIG` environment variable to configure the formatter with JSON.
8+
9+
### Configuration Schema
10+
11+
```jsonc
12+
{
13+
"outputDir": "./reports", // Optional: Output directory (default: cwd/.eslint)
14+
"filename": "eslint-report", // Optional: Base filename without extension (default: 'eslint-report')
15+
"formats": ["json"], // Optional: Array of format names for file output (default: ['json'])
16+
"terminal": "stylish", // Optional: Format for terminal output (default: 'stylish')
17+
"verbose": true, // Optional: Enable verbose logging (default: false)
18+
}
19+
```
20+
21+
### Supported Formats
22+
23+
The following ESLint formatters are supported:
24+
25+
- `stylish` (default terminal output)
26+
- `json` (default file output)
27+
- Custom formatters (fallback to stylish formatting)
28+
29+
## Usage Examples
30+
31+
### Basic Usage
32+
33+
```bash
34+
# Default behavior - JSON file output + stylish console output
35+
npx eslint .
36+
37+
# Custom output directory and filename
38+
ESLINT_FORMATTER_CONFIG='{"outputDir":"./ci-reports","filename":"lint-results"}' npx eslint .
39+
# Creates: ci-reports/lint-results.json + terminal output
40+
```
41+
42+
### Multiple Output Formats
43+
44+
```bash
45+
# Generate JSON file
46+
ESLINT_FORMATTER_CONFIG='{"formats":["json"],"terminal":"stylish"}' npx eslint .
47+
# Creates: .eslint/eslint-report.json + terminal output
48+
49+
# Custom directory with JSON format
50+
ESLINT_FORMATTER_CONFIG='{"outputDir":"./reports","filename":"eslint-results","formats":["json"]}' npx eslint .
51+
# Creates: reports/eslint-results.json
52+
```
53+
54+
### Terminal Output Only
55+
56+
```bash
57+
# Only show terminal output, no files
58+
ESLINT_FORMATTER_CONFIG='{"formats":[],"terminal":"stylish"}' npx eslint .
59+
60+
# Different terminal format
61+
ESLINT_FORMATTER_CONFIG='{"formats":[],"terminal":"stylish"}' npx eslint .
62+
```
63+
64+
## Default Behavior
65+
66+
When no `ESLINT_FORMATTER_CONFIG` is provided, the formatter uses these defaults:
67+
68+
- **outputDir**: `./.eslint` (relative to current working directory)
69+
- **filename**: `eslint-report`
70+
- **formats**: `["json"]`
71+
- **terminal**: `stylish`
72+
- **verbose**: `false`
73+
74+
This means by default you get:
75+
76+
- A JSON file at `./.eslint/eslint-report.json`
77+
- Stylish terminal output
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import tseslint from 'typescript-eslint';
2+
import baseConfig from '../../eslint.config.js';
3+
4+
export default tseslint.config(
5+
...baseConfig,
6+
{
7+
files: ['**/*.ts'],
8+
languageOptions: {
9+
parserOptions: {
10+
projectService: true,
11+
tsconfigRootDir: import.meta.dirname,
12+
},
13+
},
14+
},
15+
{
16+
files: ['**/*.json'],
17+
rules: {
18+
'@nx/dependency-checks': ['error'],
19+
},
20+
},
21+
);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "@code-pushup/eslint-formatter-multi",
3+
"version": "0.0.1",
4+
"private": false,
5+
"type": "module",
6+
"main": "./src/index.js",
7+
"types": "./src/index.d.ts",
8+
"engines": {
9+
"node": ">=17.0.0"
10+
},
11+
"description": "ESLint formatter that supports multiple output formats and destinations simultaneously",
12+
"author": "Michael Hladky",
13+
"license": "MIT",
14+
"repository": {
15+
"type": "git",
16+
"url": "https://github.com/code-pushup/cli.git",
17+
"directory": "tools/eslint-formatter-multi"
18+
},
19+
"keywords": [
20+
"eslint",
21+
"eslint-formatter",
22+
"eslintformatter",
23+
"multi-format",
24+
"code-quality",
25+
"linting"
26+
],
27+
"files": [
28+
"src/*",
29+
"README.md"
30+
],
31+
"publishConfig": {
32+
"access": "public"
33+
},
34+
"dependencies": {
35+
"ansis": "^3.3.0",
36+
"tslib": "^2.8.1"
37+
},
38+
"peerDependencies": {
39+
"eslint": "^8.0.0 || ^9.0.0"
40+
}
41+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "eslint-formatter-multi",
3+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": "tools/eslint-formatter-multi/src",
5+
"projectType": "library",
6+
"tags": ["scope:tooling", "type:util"],
7+
"targets": {
8+
"lint": {},
9+
"unit-test": {},
10+
"build": {}
11+
}
12+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default } from './lib/multiple-formats.js';
2+
export * from './lib/multiple-formats.js';
3+
export * from './lib/utils.js';
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { ESLint } from 'eslint';
2+
import * as process from 'node:process';
3+
import type { FormatterConfig } from './types.js';
4+
import {
5+
type EslintFormat,
6+
formatTerminalOutput,
7+
findConfigFromEnv as getConfigFromEnv,
8+
persistEslintReports,
9+
} from './utils.js';
10+
11+
export const DEFAULT_OUTPUT_DIR = '.eslint';
12+
export const DEFAULT_FILENAME = 'eslint-report';
13+
export const DEFAULT_FORMATS = ['json'] as EslintFormat[];
14+
export const DEFAULT_TERMINAL = 'stylish' as EslintFormat;
15+
16+
export const DEFAULT_CONFIG: Required<
17+
Pick<
18+
FormatterConfig,
19+
'outputDir' | 'filename' | 'formats' | 'terminal' | 'verbose'
20+
>
21+
> = {
22+
outputDir: DEFAULT_OUTPUT_DIR,
23+
filename: DEFAULT_FILENAME,
24+
formats: DEFAULT_FORMATS,
25+
terminal: DEFAULT_TERMINAL,
26+
verbose: false,
27+
};
28+
29+
/**
30+
* Format ESLint results using multiple configurable formatters
31+
*
32+
* @param results - The ESLint results
33+
* @param args - The arguments passed to the formatter
34+
* @returns The formatted results for terminal display
35+
*
36+
* @example
37+
* // Basic usage:
38+
* ESLINT_FORMATTER_CONFIG='{"filename":"lint-results","formats":["json"],"terminal":"stylish"}' npx eslint .
39+
* // Creates: .eslint/eslint-results.json + terminal output
40+
*
41+
* // With custom output directory:
42+
* ESLINT_FORMATTER_CONFIG='{"outputDir":"./ci-reports","filename":"eslint-report","formats":["json","html"],"terminal":"stylish"}' nx lint utils
43+
* // Creates: ci-reports/eslint-report.json, ci-reports/eslint-report.html + terminal output
44+
*
45+
* Configuration schema:
46+
* {
47+
* "outputDir": "./reports", // Optional: Output directory (default: cwd/.eslint)
48+
* "filename": "eslint-report", // Optional: Base filename without extension (default: 'eslint-report')
49+
* "formats": ["json"], // Optional: Array of format names for file output (default: ['json'])
50+
* "terminal": "stylish" // Optional: Format for terminal output (default: 'stylish')
51+
* }
52+
*/
53+
export default async function multipleFormats(
54+
results: ESLint.LintResult[],
55+
_args?: unknown,
56+
): Promise<string> {
57+
const config = {
58+
...DEFAULT_CONFIG,
59+
...getConfigFromEnv(process.env),
60+
} satisfies FormatterConfig;
61+
62+
const {
63+
outputDir = DEFAULT_OUTPUT_DIR,
64+
filename,
65+
formats,
66+
terminal,
67+
verbose = false,
68+
} = config;
69+
70+
try {
71+
await persistEslintReports(formats, results, {
72+
outputDir,
73+
filename,
74+
verbose,
75+
});
76+
} catch (error) {
77+
if (verbose) {
78+
console.error('Error writing ESLint reports:', error);
79+
}
80+
}
81+
82+
return formatTerminalOutput(terminal, results);
83+
}

0 commit comments

Comments
 (0)