From 576ec36e5a17e8664deef4a5802a42521a8d0481 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Thu, 21 Aug 2025 23:48:01 +0200 Subject: [PATCH 01/14] feat(models): add globPath schema and types --- packages/models/src/index.ts | 1 + packages/models/src/lib/configuration.ts | 5 ++++- .../models/src/lib/implementation/schemas.ts | 18 ++++++++++++++++++ .../lib/implementation/schemas.unit.test.ts | 19 +++++++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index a305ea673..1499b9cee 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -57,6 +57,7 @@ export { export { fileNameSchema, filePathSchema, + globPathSchema, materialIconSchema, type MaterialIcon, } from './lib/implementation/schemas.js'; diff --git a/packages/models/src/lib/configuration.ts b/packages/models/src/lib/configuration.ts index e3512d2f5..7151e2ceb 100644 --- a/packages/models/src/lib/configuration.ts +++ b/packages/models/src/lib/configuration.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { globPathSchema } from './implementation/schemas.js'; /** * Generic schema for a tool command configuration, reusable across plugins. @@ -13,7 +14,9 @@ export const artifactGenerationCommandSchema = z.union([ export const pluginArtifactOptionsSchema = z.object({ generateArtifactsCommand: artifactGenerationCommandSchema.optional(), - artifactsPaths: z.union([z.string(), z.array(z.string()).min(1)]), + artifactsPaths: z + .union([globPathSchema, z.array(globPathSchema).min(1)]) + .describe('File paths or glob patterns for artifact files'), }); export type PluginArtifactOptions = z.infer; diff --git a/packages/models/src/lib/implementation/schemas.ts b/packages/models/src/lib/implementation/schemas.ts index 720dec5c4..7bc128830 100644 --- a/packages/models/src/lib/implementation/schemas.ts +++ b/packages/models/src/lib/implementation/schemas.ts @@ -138,6 +138,24 @@ export const filePathSchema = z .trim() .min(1, { message: 'The path is invalid' }); +/** + * Regex for glob patterns - validates file paths and glob patterns + * Allows normal paths and paths with glob metacharacters: *, **, {}, [], !, ? + * Excludes invalid path characters: <>"| + */ +const globRegex = /^!?[^<>"|]+$/; + +/** Schema for a glob pattern (supports wildcards like *, **, {}, !, etc.) */ +export const globPathSchema = z + .string() + .trim() + .min(1, { message: 'The glob pattern is invalid' }) + .regex(globRegex, { + message: + 'The path must be a valid file path or glob pattern (supports *, **, {}, [], !, ?)', + }) + .describe('File path or glob pattern (supports *, **, {}, !, etc.)'); + /** Schema for a fileNameSchema */ export const fileNameSchema = z .string() diff --git a/packages/models/src/lib/implementation/schemas.unit.test.ts b/packages/models/src/lib/implementation/schemas.unit.test.ts index a72640350..d3703669c 100644 --- a/packages/models/src/lib/implementation/schemas.unit.test.ts +++ b/packages/models/src/lib/implementation/schemas.unit.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import { type TableCellValue, docsUrlSchema, + globPathSchema, tableCellValueSchema, weightSchema, } from './schemas.js'; @@ -66,3 +67,21 @@ describe('docsUrlSchema', () => { ); }); }); + +describe('globPathSchema', () => { + it.each([ + '**/*.ts', + 'src/components/*.jsx', + '{src,lib,test}/**/*.js', + '!node_modules/**', + ])('should accept a valid glob pattern: %s', pattern => { + expect(() => globPathSchema.parse(pattern)).not.toThrow(); + }); + + it.each(['pathfile.js', 'path"file.js', 'path|file.js'])( + 'should throw for invalid path with forbidden character: %s', + pattern => { + expect(() => globPathSchema.parse(pattern)).toThrow(); + }, + ); +}); From 0a333ef67eed15e3034dc69fbc0a621011299068 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Thu, 21 Aug 2025 23:48:15 +0200 Subject: [PATCH 02/14] feat(models): export PluginArtifactOptions --- packages/models/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 1499b9cee..aa36975c9 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -146,4 +146,5 @@ export { uploadConfigSchema, type UploadConfig } from './lib/upload-config.js'; export { artifactGenerationCommandSchema, pluginArtifactOptionsSchema, + type PluginArtifactOptions, } from './lib/configuration.js'; From 577d7195f6cc8f36b15f38302a9c324e06484d7d Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Thu, 21 Aug 2025 23:48:51 +0200 Subject: [PATCH 03/14] feat(plugin-eslint): add loadArtifacts helper --- .../plugin-eslint/src/lib/runner/utils.ts | 53 ++++++ .../src/lib/runner/utils.unit.test.ts | 162 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 packages/plugin-eslint/src/lib/runner/utils.ts create mode 100644 packages/plugin-eslint/src/lib/runner/utils.unit.test.ts diff --git a/packages/plugin-eslint/src/lib/runner/utils.ts b/packages/plugin-eslint/src/lib/runner/utils.ts new file mode 100644 index 000000000..793fb8c82 --- /dev/null +++ b/packages/plugin-eslint/src/lib/runner/utils.ts @@ -0,0 +1,53 @@ +import type { ESLint } from 'eslint'; +import { glob } from 'glob'; +import type { PluginArtifactOptions } from '@code-pushup/models'; +import { + executeProcess, + pluralizeToken, + readJsonFile, + ui, +} from '@code-pushup/utils'; +import type { LinterOutput } from './types.js'; + +export async function loadArtifacts( + artifacts: PluginArtifactOptions, +): Promise { + if (artifacts.generateArtifactsCommand) { + const { command, args = [] } = + typeof artifacts.generateArtifactsCommand === 'string' + ? { command: artifacts.generateArtifactsCommand } + : artifacts.generateArtifactsCommand; + + const commandString = + typeof artifacts.generateArtifactsCommand === 'string' + ? artifacts.generateArtifactsCommand + : `${command} ${args.join(' ')}`; + await ui().logger.log(`$ ${commandString}`); + await executeProcess({ + command, + args, + ignoreExitCode: true, + }); + } + + const initialArtifactPaths = Array.isArray(artifacts.artifactsPaths) + ? artifacts.artifactsPaths + : [artifacts.artifactsPaths]; + + const artifactPaths = await glob(initialArtifactPaths, options); + + ui().logger.log( + `ESLint plugin resolved ${initialArtifactPaths.length} ${pluralizeToken('pattern', initialArtifactPaths.length)} to ${artifactPaths.length} eslint ${pluralizeToken('report', artifactPaths.length)}`, + ); + + return await Promise.all( + artifactPaths.map(async artifactPath => { + // ESLint CLI outputs raw ESLint.LintResult[], but we need LinterOutput format + const results = await readJsonFile(artifactPath); + return { + results, + ruleOptionsPerFile: {}, // TODO + }; + }), + ); +} diff --git a/packages/plugin-eslint/src/lib/runner/utils.unit.test.ts b/packages/plugin-eslint/src/lib/runner/utils.unit.test.ts new file mode 100644 index 000000000..87f9325ff --- /dev/null +++ b/packages/plugin-eslint/src/lib/runner/utils.unit.test.ts @@ -0,0 +1,162 @@ +import type { ESLint } from 'eslint'; +import * as globModule from 'glob'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { ui } from '@code-pushup/utils'; +import * as utilsModule from '@code-pushup/utils'; +import type { LinterOutput } from './types.js'; +import { loadArtifacts } from './utils.js'; + +describe('loadArtifacts', () => { + const globSpy = vi.spyOn(globModule, 'glob'); + const readJsonFileSpy = vi.spyOn(utilsModule, 'readJsonFile'); + const executeProcessSpy = vi.spyOn(utilsModule, 'executeProcess'); + + const mockRawResults1: ESLint.LintResult[] = [ + { + filePath: '/test/file1.js', + messages: [ + { + ruleId: 'no-unused-vars', + line: 1, + column: 7, + message: 'unused variable', + severity: 2, + }, + ], + suppressedMessages: [], + errorCount: 1, + fatalErrorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: 'const unused = 1;', + usedDeprecatedRules: [], + }, + ]; + + const mockRawResults2: ESLint.LintResult[] = [ + { + filePath: '/test/file2.js', + messages: [], + suppressedMessages: [], + errorCount: 0, + fatalErrorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: 'const valid = 1; console.log(valid);', + usedDeprecatedRules: [], + }, + ]; + + const expectedLinterOutput1: LinterOutput = { + results: mockRawResults1, + ruleOptionsPerFile: {}, + }; + + const expectedLinterOutput2: LinterOutput = { + results: mockRawResults2, + ruleOptionsPerFile: {}, + }; + + const artifactsPaths = ['/path/to/artifact1.json', '/path/to/artifact2.json']; + + beforeEach(() => { + vi.clearAllMocks(); + executeProcessSpy.mockResolvedValue({ + stdout: JSON.stringify(mockRawResults1), + stderr: '', + code: 0, + date: new Date().toISOString(), + duration: 0, + }); + }); + + it('should load single artifact without generateArtifactsCommand', async () => { + globSpy.mockResolvedValue([artifactsPaths.at(0)!]); + readJsonFileSpy.mockResolvedValue(mockRawResults1); + + await expect( + loadArtifacts({ artifactsPaths: artifactsPaths.at(0)! }), + ).resolves.toStrictEqual([expectedLinterOutput1]); + expect(executeProcessSpy).not.toHaveBeenCalled(); + expect(readJsonFileSpy).toHaveBeenCalledTimes(1); + expect(readJsonFileSpy).toHaveBeenNthCalledWith(1, artifactsPaths.at(0)); + + expect(ui()).not.toHaveLogged('log', expect.stringMatching(/^\$ /)); + }); + + it('should load multiple artifacts without generateArtifactsCommand', async () => { + globSpy + .mockResolvedValueOnce([artifactsPaths.at(0)!]) + .mockResolvedValueOnce([artifactsPaths.at(1)!]); + readJsonFileSpy + .mockResolvedValueOnce(mockRawResults1) + .mockResolvedValueOnce(mockRawResults2); + + await expect(loadArtifacts({ artifactsPaths })).resolves.toStrictEqual([ + expectedLinterOutput1, + expectedLinterOutput2, + ]); + + expect(globSpy).toHaveBeenCalledTimes(2); + expect(globSpy).toHaveBeenNthCalledWith(1, artifactsPaths.at(0)); + expect(globSpy).toHaveBeenNthCalledWith(2, artifactsPaths.at(1)); + expect(readJsonFileSpy).toHaveBeenCalledTimes(2); + expect(readJsonFileSpy).toHaveBeenNthCalledWith(1, artifactsPaths.at(0)); + expect(readJsonFileSpy).toHaveBeenNthCalledWith(2, artifactsPaths.at(1)); + expect(executeProcessSpy).not.toHaveBeenCalled(); + expect(ui()).not.toHaveLogged('log', expect.stringMatching(/^\$ /)); + }); + + it('should load artifacts with generateArtifactsCommand as string', async () => { + globSpy + .mockResolvedValueOnce([artifactsPaths.at(0)!]) + .mockResolvedValueOnce([artifactsPaths.at(1)!]); + readJsonFileSpy.mockResolvedValue([]); + + const generateArtifactsCommand = `nx run-many -t lint --parallel --max-parallel=5`; + await expect( + loadArtifacts({ + artifactsPaths, + generateArtifactsCommand, + }), + ).resolves.not.toThrow(); + + expect(executeProcessSpy).toHaveBeenCalledWith({ + args: [], + command: generateArtifactsCommand, + ignoreExitCode: true, + }); + expect(globSpy).toHaveBeenCalledTimes(2); + expect(ui()).toHaveLogged('log', `$ ${generateArtifactsCommand}`); + }); + + it('should load artifacts with generateArtifactsCommand as object', async () => { + globSpy + .mockResolvedValueOnce([artifactsPaths.at(0)!]) + .mockResolvedValueOnce([artifactsPaths.at(1)!]); + readJsonFileSpy.mockResolvedValue([]); + + const generateArtifactsCommand = { + command: 'nx', + args: ['run-many', '-t', 'lint', '--parallel', '--max-parallel=5'], + }; + await expect( + loadArtifacts({ + artifactsPaths, + generateArtifactsCommand, + }), + ).resolves.not.toThrow(); + + expect(executeProcessSpy).toHaveBeenCalledWith({ + ...generateArtifactsCommand, + ignoreExitCode: true, + }); + expect(globSpy).toHaveBeenCalledTimes(2); + expect(ui()).toHaveLogged( + 'log', + '$ nx run-many -t lint --parallel --max-parallel=5', + ); + }); +}); From 31072f8a669e90c6e4c9972692de0fd2e07e5576 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Thu, 21 Aug 2025 23:49:02 +0200 Subject: [PATCH 04/14] feat(plugin-eslint): use loadArtifacts helper --- .../plugin-eslint/src/lib/runner/index.ts | 57 ++++++- .../src/lib/runner/index.unit.test.ts | 150 ++++++++++++++++++ 2 files changed, 202 insertions(+), 5 deletions(-) create mode 100644 packages/plugin-eslint/src/lib/runner/index.unit.test.ts diff --git a/packages/plugin-eslint/src/lib/runner/index.ts b/packages/plugin-eslint/src/lib/runner/index.ts index 7e5e8ee9d..6c59497fd 100644 --- a/packages/plugin-eslint/src/lib/runner/index.ts +++ b/packages/plugin-eslint/src/lib/runner/index.ts @@ -1,10 +1,13 @@ import { writeFile } from 'node:fs/promises'; import path from 'node:path'; -import type { - Audit, - AuditOutput, - RunnerConfig, - RunnerFilesPaths, +import { + type Audit, + type AuditOutput, + type AuditOutputs, + DEFAULT_PERSIST_OUTPUT_DIR, + type PluginArtifactOptions, + type RunnerConfig, + type RunnerFilesPaths, } from '@code-pushup/models'; import { asyncSequential, @@ -18,6 +21,7 @@ import { import type { ESLintPluginRunnerConfig, ESLintTarget } from '../config.js'; import { lint } from './lint.js'; import { lintResultsToAudits, mergeLinterOutputs } from './transform.js'; +import { loadArtifacts } from './utils.js'; export async function executeRunner({ runnerConfigPath, @@ -71,3 +75,46 @@ export async function createRunnerConfig( outputFile: runnerOutputPath, }; } + +export async function generateAuditOutputs(options: { + audits: Audit[]; + targets: ESLintTarget[]; + artifacts?: PluginArtifactOptions; + outputDir?: string; +}): Promise { + const { + audits, + targets, + artifacts, + outputDir = DEFAULT_PERSIST_OUTPUT_DIR, + } = options; + const config: ESLintPluginRunnerConfig = { + targets, + slugs: audits.map(audit => audit.slug), + }; + + ui().logger.log(`ESLint plugin executing ${targets.length} lint targets`); + + const linterOutputs = artifacts + ? await loadArtifacts(artifacts) + : await asyncSequential( + targets.map(target => ({ + ...target, + outputDir, + })), + lint, + ); + const lintResults = mergeLinterOutputs(linterOutputs); + const failedAudits = lintResultsToAudits(lintResults); + + return config.slugs.map( + (slug): AuditOutput => + failedAudits.find(audit => audit.slug === slug) ?? { + slug, + score: 1, + value: 0, + displayValue: 'passed', + details: { issues: [] }, + }, + ); +} diff --git a/packages/plugin-eslint/src/lib/runner/index.unit.test.ts b/packages/plugin-eslint/src/lib/runner/index.unit.test.ts new file mode 100644 index 000000000..07e12f5c1 --- /dev/null +++ b/packages/plugin-eslint/src/lib/runner/index.unit.test.ts @@ -0,0 +1,150 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { z } from 'zod'; +import type { + Audit, + AuditOutput, + pluginArtifactOptionsSchema, +} from '@code-pushup/models'; +import { ui } from '@code-pushup/utils'; +import type { ESLintTarget } from '../config.js'; +import { generateAuditOutputs } from './index.js'; +import * as lintModule from './lint.js'; +import type { LinterOutput } from './types.js'; +import * as utilsFileModule from './utils.js'; + +describe('generateAuditOutputs', () => { + const loadArtifactsSpy = vi.spyOn(utilsFileModule, 'loadArtifacts'); + const lintSpy = vi.spyOn(lintModule, 'lint'); + + const mockAudits: Audit[] = [ + { slug: 'max-lines', title: 'Max lines', description: 'Test' }, + { slug: 'no-unused-vars', title: 'No unused vars', description: 'Test' }, + ]; + const mockTargetPatterns = { patterns: ['src/**/*.ts'] }; + const mockTargetPatternsAndConfigs = { + patterns: ['lib/**/*.js'], + eslintrc: '.eslintrc.js', + }; + const mockTargets: ESLintTarget[] = [ + mockTargetPatterns, + mockTargetPatternsAndConfigs, + ]; + + const mockLinterOutputs: LinterOutput[] = [ + { + results: [ + { + filePath: 'test.ts', + messages: [ + { + ruleId: 'max-lines', + severity: 1, + message: 'File has too many lines', + line: 1, + column: 1, + }, + ], + } as any, + ], + ruleOptionsPerFile: { 'test.ts': { 'max-lines': [] } }, + }, + { + results: [ + { + filePath: 'test.ts', + messages: [ + { + ruleId: 'max-lines', + severity: 1, + message: 'File has too many lines', + line: 1, + column: 1, + }, + ], + } as any, + ], + ruleOptionsPerFile: { 'test.ts': { 'max-lines': [] } }, + }, + ]; + + const mockedAuditOutputs: AuditOutput[] = [ + { + slug: 'max-lines', + score: 0, + value: 2, + displayValue: '2 warnings', + details: { + issues: [ + { + message: 'File has too many lines', + severity: 'warning', + source: { + file: 'test.ts', + position: { + startLine: 1, + startColumn: 1, + }, + }, + }, + { + message: 'File has too many lines', + severity: 'warning', + source: { + file: 'test.ts', + position: { + startLine: 1, + startColumn: 1, + }, + }, + }, + ], + }, + }, + { + slug: 'no-unused-vars', + score: 1, + value: 0, + displayValue: 'passed', + details: { issues: [] }, + }, + ]; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should use loadArtifacts when artifacts are provided', async () => { + const artifacts: z.infer = { + artifactsPaths: ['path/to/artifacts.json'], + }; + loadArtifactsSpy.mockResolvedValue(mockLinterOutputs); + + await expect( + generateAuditOutputs({ + audits: mockAudits, + targets: mockTargets, + artifacts, + }), + ).resolves.toStrictEqual(mockedAuditOutputs); + + expect(loadArtifactsSpy).toHaveBeenCalledWith(artifacts); + expect(lintSpy).not.toHaveBeenCalled(); + expect(ui()).toHaveLogged('log', 'ESLint plugin executing 2 lint targets'); + }); + + it('should use internal linting logic when artifacts are not provided', async () => { + lintSpy.mockResolvedValueOnce(mockLinterOutputs.at(0)!); + lintSpy.mockResolvedValueOnce(mockLinterOutputs.at(0)!); + + await expect( + generateAuditOutputs({ + audits: mockAudits, + targets: mockTargets, + outputDir: 'custom-output', + }), + ).resolves.toStrictEqual(mockedAuditOutputs); + + expect(loadArtifactsSpy).not.toHaveBeenCalled(); + expect(lintSpy).toHaveBeenCalledTimes(2); + }); +}); From fd29da9ec38e49f1695826cdb6ba4873642e2ac4 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Thu, 21 Aug 2025 23:59:05 +0200 Subject: [PATCH 05/14] test(models): fix unit tests --- packages/models/src/lib/implementation/schemas.unit.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/models/src/lib/implementation/schemas.unit.test.ts b/packages/models/src/lib/implementation/schemas.unit.test.ts index d3703669c..818797d82 100644 --- a/packages/models/src/lib/implementation/schemas.unit.test.ts +++ b/packages/models/src/lib/implementation/schemas.unit.test.ts @@ -81,7 +81,9 @@ describe('globPathSchema', () => { it.each(['pathfile.js', 'path"file.js', 'path|file.js'])( 'should throw for invalid path with forbidden character: %s', pattern => { - expect(() => globPathSchema.parse(pattern)).toThrow(); + expect(() => globPathSchema.parse(pattern)).toThrow( + 'valid file path or glob pattern', + ); }, ); }); From 6c657a5c7e3d92ed114f36dac59e3b1a59911be7 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Thu, 21 Aug 2025 23:59:21 +0200 Subject: [PATCH 06/14] test(plugin-eslint): fix unit tests and lint --- packages/plugin-eslint/package.json | 1 + .../plugin-eslint/src/lib/runner/utils.ts | 2 +- .../src/lib/runner/utils.unit.test.ts | 23 ++++++++----------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/plugin-eslint/package.json b/packages/plugin-eslint/package.json index c06d1a1c1..f8a2f6332 100644 --- a/packages/plugin-eslint/package.json +++ b/packages/plugin-eslint/package.json @@ -40,6 +40,7 @@ "dependencies": { "@code-pushup/utils": "0.74.1", "@code-pushup/models": "0.74.1", + "glob": "^11.0.0", "yargs": "^17.7.2", "zod": "^4.0.5" }, diff --git a/packages/plugin-eslint/src/lib/runner/utils.ts b/packages/plugin-eslint/src/lib/runner/utils.ts index 793fb8c82..bb99a1450 100644 --- a/packages/plugin-eslint/src/lib/runner/utils.ts +++ b/packages/plugin-eslint/src/lib/runner/utils.ts @@ -34,7 +34,7 @@ export async function loadArtifacts( ? artifacts.artifactsPaths : [artifacts.artifactsPaths]; - const artifactPaths = await glob(initialArtifactPaths, options); + const artifactPaths = await glob(initialArtifactPaths); ui().logger.log( `ESLint plugin resolved ${initialArtifactPaths.length} ${pluralizeToken('pattern', initialArtifactPaths.length)} to ${artifactPaths.length} eslint ${pluralizeToken('report', artifactPaths.length)}`, diff --git a/packages/plugin-eslint/src/lib/runner/utils.unit.test.ts b/packages/plugin-eslint/src/lib/runner/utils.unit.test.ts index 87f9325ff..fd097da3d 100644 --- a/packages/plugin-eslint/src/lib/runner/utils.unit.test.ts +++ b/packages/plugin-eslint/src/lib/runner/utils.unit.test.ts @@ -87,9 +87,7 @@ describe('loadArtifacts', () => { }); it('should load multiple artifacts without generateArtifactsCommand', async () => { - globSpy - .mockResolvedValueOnce([artifactsPaths.at(0)!]) - .mockResolvedValueOnce([artifactsPaths.at(1)!]); + globSpy.mockResolvedValue(artifactsPaths); readJsonFileSpy .mockResolvedValueOnce(mockRawResults1) .mockResolvedValueOnce(mockRawResults2); @@ -99,9 +97,8 @@ describe('loadArtifacts', () => { expectedLinterOutput2, ]); - expect(globSpy).toHaveBeenCalledTimes(2); - expect(globSpy).toHaveBeenNthCalledWith(1, artifactsPaths.at(0)); - expect(globSpy).toHaveBeenNthCalledWith(2, artifactsPaths.at(1)); + expect(globSpy).toHaveBeenCalledTimes(1); + expect(globSpy).toHaveBeenCalledWith(artifactsPaths); expect(readJsonFileSpy).toHaveBeenCalledTimes(2); expect(readJsonFileSpy).toHaveBeenNthCalledWith(1, artifactsPaths.at(0)); expect(readJsonFileSpy).toHaveBeenNthCalledWith(2, artifactsPaths.at(1)); @@ -110,9 +107,7 @@ describe('loadArtifacts', () => { }); it('should load artifacts with generateArtifactsCommand as string', async () => { - globSpy - .mockResolvedValueOnce([artifactsPaths.at(0)!]) - .mockResolvedValueOnce([artifactsPaths.at(1)!]); + globSpy.mockResolvedValue(artifactsPaths); readJsonFileSpy.mockResolvedValue([]); const generateArtifactsCommand = `nx run-many -t lint --parallel --max-parallel=5`; @@ -128,14 +123,13 @@ describe('loadArtifacts', () => { command: generateArtifactsCommand, ignoreExitCode: true, }); - expect(globSpy).toHaveBeenCalledTimes(2); + expect(globSpy).toHaveBeenCalledTimes(1); + expect(globSpy).toHaveBeenCalledWith(artifactsPaths); expect(ui()).toHaveLogged('log', `$ ${generateArtifactsCommand}`); }); it('should load artifacts with generateArtifactsCommand as object', async () => { - globSpy - .mockResolvedValueOnce([artifactsPaths.at(0)!]) - .mockResolvedValueOnce([artifactsPaths.at(1)!]); + globSpy.mockResolvedValue(artifactsPaths); readJsonFileSpy.mockResolvedValue([]); const generateArtifactsCommand = { @@ -153,7 +147,8 @@ describe('loadArtifacts', () => { ...generateArtifactsCommand, ignoreExitCode: true, }); - expect(globSpy).toHaveBeenCalledTimes(2); + expect(globSpy).toHaveBeenCalledTimes(1); + expect(globSpy).toHaveBeenCalledWith(artifactsPaths); expect(ui()).toHaveLogged( 'log', '$ nx run-many -t lint --parallel --max-parallel=5', From 47adbb44a0b16e3f002fd41614777d7b86273cb7 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Fri, 22 Aug 2025 00:32:12 +0200 Subject: [PATCH 07/14] fix(plugin-eslint): remove path resolved log --- packages/plugin-eslint/src/lib/runner/utils.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/packages/plugin-eslint/src/lib/runner/utils.ts b/packages/plugin-eslint/src/lib/runner/utils.ts index bb99a1450..0761ba399 100644 --- a/packages/plugin-eslint/src/lib/runner/utils.ts +++ b/packages/plugin-eslint/src/lib/runner/utils.ts @@ -1,12 +1,7 @@ import type { ESLint } from 'eslint'; import { glob } from 'glob'; import type { PluginArtifactOptions } from '@code-pushup/models'; -import { - executeProcess, - pluralizeToken, - readJsonFile, - ui, -} from '@code-pushup/utils'; +import { executeProcess, readJsonFile, ui } from '@code-pushup/utils'; import type { LinterOutput } from './types.js'; export async function loadArtifacts( @@ -36,10 +31,6 @@ export async function loadArtifacts( const artifactPaths = await glob(initialArtifactPaths); - ui().logger.log( - `ESLint plugin resolved ${initialArtifactPaths.length} ${pluralizeToken('pattern', initialArtifactPaths.length)} to ${artifactPaths.length} eslint ${pluralizeToken('report', artifactPaths.length)}`, - ); - return await Promise.all( artifactPaths.map(async artifactPath => { // ESLint CLI outputs raw ESLint.LintResult[], but we need LinterOutput format From 03db1b91bcd9a5891604143044a934fa09ed0fa2 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Fri, 22 Aug 2025 00:40:29 +0200 Subject: [PATCH 08/14] fix: refine logic --- packages/models/src/lib/implementation/schemas.ts | 5 +++-- packages/plugin-eslint/src/lib/runner/index.ts | 9 +-------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/models/src/lib/implementation/schemas.ts b/packages/models/src/lib/implementation/schemas.ts index 7bc128830..fea732ac0 100644 --- a/packages/models/src/lib/implementation/schemas.ts +++ b/packages/models/src/lib/implementation/schemas.ts @@ -145,7 +145,6 @@ export const filePathSchema = z */ const globRegex = /^!?[^<>"|]+$/; -/** Schema for a glob pattern (supports wildcards like *, **, {}, !, etc.) */ export const globPathSchema = z .string() .trim() @@ -154,7 +153,9 @@ export const globPathSchema = z message: 'The path must be a valid file path or glob pattern (supports *, **, {}, [], !, ?)', }) - .describe('File path or glob pattern (supports *, **, {}, !, etc.)'); + .describe( + 'Schema for a glob pattern (supports wildcards like *, **, {}, !, etc.)', + ); /** Schema for a fileNameSchema */ export const fileNameSchema = z diff --git a/packages/plugin-eslint/src/lib/runner/index.ts b/packages/plugin-eslint/src/lib/runner/index.ts index 6c59497fd..f7d432bd6 100644 --- a/packages/plugin-eslint/src/lib/runner/index.ts +++ b/packages/plugin-eslint/src/lib/runner/index.ts @@ -80,14 +80,8 @@ export async function generateAuditOutputs(options: { audits: Audit[]; targets: ESLintTarget[]; artifacts?: PluginArtifactOptions; - outputDir?: string; }): Promise { - const { - audits, - targets, - artifacts, - outputDir = DEFAULT_PERSIST_OUTPUT_DIR, - } = options; + const { audits, targets, artifacts } = options; const config: ESLintPluginRunnerConfig = { targets, slugs: audits.map(audit => audit.slug), @@ -100,7 +94,6 @@ export async function generateAuditOutputs(options: { : await asyncSequential( targets.map(target => ({ ...target, - outputDir, })), lint, ); From cd3ee270773c2ecfe780c8bc6a3ad6dbca349022 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Fri, 22 Aug 2025 00:51:30 +0200 Subject: [PATCH 09/14] fix: wip --- nx.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nx.json b/nx.json index 99e48af53..bee64f9c3 100644 --- a/nx.json +++ b/nx.json @@ -40,9 +40,7 @@ } } }, - "e2e": { - "dependsOn": ["^build"] - }, + "e2e": { "dependsOn": ["^build"] }, "lint": { "inputs": ["default", "{workspaceRoot}/eslint.config.?(c)js"], "executor": "@nx/linter:eslint", From 79e541a135f3a495f36ae1e379b1e8d589785dd1 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Fri, 22 Aug 2025 01:17:21 +0200 Subject: [PATCH 10/14] fix: fix lint issue --- packages/plugin-eslint/src/lib/runner/index.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/plugin-eslint/src/lib/runner/index.ts b/packages/plugin-eslint/src/lib/runner/index.ts index f7d432bd6..0100a94d7 100644 --- a/packages/plugin-eslint/src/lib/runner/index.ts +++ b/packages/plugin-eslint/src/lib/runner/index.ts @@ -1,13 +1,12 @@ import { writeFile } from 'node:fs/promises'; import path from 'node:path'; -import { - type Audit, - type AuditOutput, - type AuditOutputs, - DEFAULT_PERSIST_OUTPUT_DIR, - type PluginArtifactOptions, - type RunnerConfig, - type RunnerFilesPaths, +import type { + Audit, + AuditOutput, + AuditOutputs, + PluginArtifactOptions, + RunnerConfig, + RunnerFilesPaths, } from '@code-pushup/models'; import { asyncSequential, From cd34ac460e25d8179c13dc22b7049e37fa486cea Mon Sep 17 00:00:00 2001 From: Michael Hladky <10064416+BioPhoton@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:32:27 +0200 Subject: [PATCH 11/14] Update packages/plugin-eslint/src/lib/runner/index.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Matěj Chalk <34691111+matejchalk@users.noreply.github.com> --- packages/plugin-eslint/src/lib/runner/index.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/plugin-eslint/src/lib/runner/index.ts b/packages/plugin-eslint/src/lib/runner/index.ts index 0100a94d7..0ae6afd3b 100644 --- a/packages/plugin-eslint/src/lib/runner/index.ts +++ b/packages/plugin-eslint/src/lib/runner/index.ts @@ -90,12 +90,7 @@ export async function generateAuditOutputs(options: { const linterOutputs = artifacts ? await loadArtifacts(artifacts) - : await asyncSequential( - targets.map(target => ({ - ...target, - })), - lint, - ); + : await asyncSequential(targets, lint); const lintResults = mergeLinterOutputs(linterOutputs); const failedAudits = lintResultsToAudits(lintResults); From 58bdc6fad78a90e0991752d466cda25b56821229 Mon Sep 17 00:00:00 2001 From: Michael Hladky <10064416+BioPhoton@users.noreply.github.com> Date: Fri, 22 Aug 2025 16:34:33 +0200 Subject: [PATCH 12/14] Update packages/plugin-eslint/src/lib/runner/utils.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Matěj Chalk <34691111+matejchalk@users.noreply.github.com> --- packages/plugin-eslint/src/lib/runner/utils.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/plugin-eslint/src/lib/runner/utils.ts b/packages/plugin-eslint/src/lib/runner/utils.ts index 0761ba399..86d4acdfc 100644 --- a/packages/plugin-eslint/src/lib/runner/utils.ts +++ b/packages/plugin-eslint/src/lib/runner/utils.ts @@ -13,11 +13,6 @@ export async function loadArtifacts( ? { command: artifacts.generateArtifactsCommand } : artifacts.generateArtifactsCommand; - const commandString = - typeof artifacts.generateArtifactsCommand === 'string' - ? artifacts.generateArtifactsCommand - : `${command} ${args.join(' ')}`; - await ui().logger.log(`$ ${commandString}`); await executeProcess({ command, args, From 638f4a3f80fcb237e5ada146413655819ec665c4 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Fri, 22 Aug 2025 16:38:56 +0200 Subject: [PATCH 13/14] refactor: fix tests --- packages/plugin-eslint/src/lib/runner/utils.unit.test.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/plugin-eslint/src/lib/runner/utils.unit.test.ts b/packages/plugin-eslint/src/lib/runner/utils.unit.test.ts index fd097da3d..e6b1ed630 100644 --- a/packages/plugin-eslint/src/lib/runner/utils.unit.test.ts +++ b/packages/plugin-eslint/src/lib/runner/utils.unit.test.ts @@ -82,8 +82,6 @@ describe('loadArtifacts', () => { expect(executeProcessSpy).not.toHaveBeenCalled(); expect(readJsonFileSpy).toHaveBeenCalledTimes(1); expect(readJsonFileSpy).toHaveBeenNthCalledWith(1, artifactsPaths.at(0)); - - expect(ui()).not.toHaveLogged('log', expect.stringMatching(/^\$ /)); }); it('should load multiple artifacts without generateArtifactsCommand', async () => { @@ -103,7 +101,6 @@ describe('loadArtifacts', () => { expect(readJsonFileSpy).toHaveBeenNthCalledWith(1, artifactsPaths.at(0)); expect(readJsonFileSpy).toHaveBeenNthCalledWith(2, artifactsPaths.at(1)); expect(executeProcessSpy).not.toHaveBeenCalled(); - expect(ui()).not.toHaveLogged('log', expect.stringMatching(/^\$ /)); }); it('should load artifacts with generateArtifactsCommand as string', async () => { @@ -125,7 +122,6 @@ describe('loadArtifacts', () => { }); expect(globSpy).toHaveBeenCalledTimes(1); expect(globSpy).toHaveBeenCalledWith(artifactsPaths); - expect(ui()).toHaveLogged('log', `$ ${generateArtifactsCommand}`); }); it('should load artifacts with generateArtifactsCommand as object', async () => { @@ -149,9 +145,5 @@ describe('loadArtifacts', () => { }); expect(globSpy).toHaveBeenCalledTimes(1); expect(globSpy).toHaveBeenCalledWith(artifactsPaths); - expect(ui()).toHaveLogged( - 'log', - '$ nx run-many -t lint --parallel --max-parallel=5', - ); }); }); From 1825a65070269d86d3055c618d4f604dadba8ff7 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Fri, 22 Aug 2025 19:10:32 +0200 Subject: [PATCH 14/14] refactor: fix lint --- packages/plugin-eslint/src/lib/runner/utils.ts | 2 +- packages/plugin-eslint/src/lib/runner/utils.unit.test.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/plugin-eslint/src/lib/runner/utils.ts b/packages/plugin-eslint/src/lib/runner/utils.ts index 86d4acdfc..2e23354af 100644 --- a/packages/plugin-eslint/src/lib/runner/utils.ts +++ b/packages/plugin-eslint/src/lib/runner/utils.ts @@ -1,7 +1,7 @@ import type { ESLint } from 'eslint'; import { glob } from 'glob'; import type { PluginArtifactOptions } from '@code-pushup/models'; -import { executeProcess, readJsonFile, ui } from '@code-pushup/utils'; +import { executeProcess, readJsonFile } from '@code-pushup/utils'; import type { LinterOutput } from './types.js'; export async function loadArtifacts( diff --git a/packages/plugin-eslint/src/lib/runner/utils.unit.test.ts b/packages/plugin-eslint/src/lib/runner/utils.unit.test.ts index e6b1ed630..04b868a16 100644 --- a/packages/plugin-eslint/src/lib/runner/utils.unit.test.ts +++ b/packages/plugin-eslint/src/lib/runner/utils.unit.test.ts @@ -1,7 +1,6 @@ import type { ESLint } from 'eslint'; import * as globModule from 'glob'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { ui } from '@code-pushup/utils'; import * as utilsModule from '@code-pushup/utils'; import type { LinterOutput } from './types.js'; import { loadArtifacts } from './utils.js';