From c2f65e6a39a53d1217ab3680e2f317613a2b917f Mon Sep 17 00:00:00 2001 From: Vojtech Masek Date: Mon, 3 Feb 2025 13:00:44 +0100 Subject: [PATCH 1/4] fix: nest files with timestamp for plugin runners --- .../core/src/lib/implementation/runner.ts | 11 ++++- packages/models/src/index.ts | 2 + packages/models/src/lib/runner-config.ts | 10 ++++- .../src/executors/cli/executor.unit.test.ts | 3 +- packages/plugin-coverage/package.json | 1 + packages/plugin-coverage/src/bin.ts | 6 ++- .../src/lib/runner/constants.ts | 11 ----- .../plugin-coverage/src/lib/runner/index.ts | 34 ++++++++++----- .../src/lib/runner/runner.integration.test.ts | 39 +++++++++-------- packages/plugin-eslint/package.json | 1 + packages/plugin-eslint/src/bin.ts | 6 ++- .../eslint-plugin.integration.test.ts.snap | 5 ++- .../src/lib/eslint-plugin.integration.test.ts | 16 ++++++- packages/plugin-eslint/src/lib/meta/index.ts | 2 +- .../src/lib/runner.integration.test.ts | 43 +++++++++++++------ .../plugin-eslint/src/lib/runner/index.ts | 43 +++++++++++-------- packages/plugin-js-packages/package.json | 1 + packages/plugin-js-packages/src/bin.ts | 6 ++- .../src/lib/runner/constants.ts | 10 ----- .../src/lib/runner/index.ts | 30 ++++++++----- .../src/lib/runner/runner.integration.test.ts | 20 ++++++--- packages/utils/src/index.ts | 1 + packages/utils/src/lib/create-runner-files.ts | 28 ++++++++++++ packages/utils/src/lib/transform.ts | 2 +- 24 files changed, 222 insertions(+), 109 deletions(-) delete mode 100644 packages/plugin-js-packages/src/lib/runner/constants.ts create mode 100644 packages/utils/src/lib/create-runner-files.ts diff --git a/packages/core/src/lib/implementation/runner.ts b/packages/core/src/lib/implementation/runner.ts index 1cde8cae0..facb4903b 100644 --- a/packages/core/src/lib/implementation/runner.ts +++ b/packages/core/src/lib/implementation/runner.ts @@ -4,7 +4,12 @@ import type { RunnerConfig, RunnerFunction, } from '@code-pushup/models'; -import { calcDuration, executeProcess, readJsonFile } from '@code-pushup/utils'; +import { + calcDuration, + executeProcess, + readJsonFile, + removeDirectoryIfExists, +} from '@code-pushup/utils'; export type RunnerResult = { date: string; @@ -26,7 +31,9 @@ export async function executeRunnerConfig( }); // read process output from file system and parse it - const outputs = await readJsonFile(path.join(process.cwd(), outputFile)); + const outputs = await readJsonFile(outputFile); + // clean up plugin individual runner output directory + await removeDirectoryIfExists(path.dirname(outputFile)); // transform unknownAuditOutputs to auditOutputs const audits = outputTransform ? await outputTransform(outputs) : outputs; diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 2764f8fb1..b5ce207ce 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -99,9 +99,11 @@ export { onProgressSchema, runnerConfigSchema, runnerFunctionSchema, + runnerFilesPathsSchema, type OnProgress, type RunnerConfig, type RunnerFunction, + type RunnerFilesPaths, } from './lib/runner-config.js'; export { tableAlignmentSchema, diff --git a/packages/models/src/lib/runner-config.ts b/packages/models/src/lib/runner-config.ts index 6c5239841..7e9af7bb3 100644 --- a/packages/models/src/lib/runner-config.ts +++ b/packages/models/src/lib/runner-config.ts @@ -15,8 +15,9 @@ export const runnerConfigSchema = z.object( description: 'Shell command to execute', }), args: z.array(z.string({ description: 'Command arguments' })).optional(), - outputFile: filePathSchema.describe('Output path'), + outputFile: filePathSchema.describe('Runner output path'), outputTransform: outputTransformSchema.optional(), + configFile: filePathSchema.describe('Runner config path').optional(), }, { description: 'How to execute runner', @@ -37,3 +38,10 @@ export const runnerFunctionSchema = z .returns(z.union([auditOutputsSchema, z.promise(auditOutputsSchema)])); export type RunnerFunction = z.infer; + +export const runnerFilesPathsSchema = z.object({ + runnerConfigPath: filePathSchema.describe('Runner config path'), + runnerOutputPath: filePathSchema.describe('Runner output path'), +}); + +export type RunnerFilesPaths = z.infer; diff --git a/packages/nx-plugin/src/executors/cli/executor.unit.test.ts b/packages/nx-plugin/src/executors/cli/executor.unit.test.ts index e538a470c..416ff91ab 100644 --- a/packages/nx-plugin/src/executors/cli/executor.unit.test.ts +++ b/packages/nx-plugin/src/executors/cli/executor.unit.test.ts @@ -2,6 +2,7 @@ import { logger } from '@nx/devkit'; import { execSync } from 'node:child_process'; import { afterEach, beforeEach, expect, vi } from 'vitest'; import { executorContext } from '@code-pushup/test-nx-utils'; +import { MEMFS_VOLUME } from '@code-pushup/test-utils'; import runAutorunExecutor from './executor.js'; vi.mock('node:child_process', async () => { @@ -39,7 +40,7 @@ describe('runAutorunExecutor', () => { // eslint-disable-next-line n/no-sync expect(execSync).toHaveBeenCalledWith( expect.stringContaining('npx @code-pushup/cli'), - { cwd: '/test' }, + { cwd: MEMFS_VOLUME }, ); }); diff --git a/packages/plugin-coverage/package.json b/packages/plugin-coverage/package.json index 2bb49ff18..512961123 100644 --- a/packages/plugin-coverage/package.json +++ b/packages/plugin-coverage/package.json @@ -38,6 +38,7 @@ "@code-pushup/utils": "0.57.0", "ansis": "^3.3.0", "parse-lcov": "^1.0.4", + "yargs": "^17.7.2", "zod": "^3.22.4" }, "peerDependencies": { diff --git a/packages/plugin-coverage/src/bin.ts b/packages/plugin-coverage/src/bin.ts index bf6572a76..fc625dd73 100644 --- a/packages/plugin-coverage/src/bin.ts +++ b/packages/plugin-coverage/src/bin.ts @@ -1,3 +1,7 @@ +import process from 'node:process'; +import { Parser } from 'yargs/helpers'; import { executeRunner } from './lib/runner/index.js'; -await executeRunner(); +const { runnerConfigPath, runnerOutputPath } = Parser(process.argv); + +await executeRunner({ runnerConfigPath, runnerOutputPath }); diff --git a/packages/plugin-coverage/src/lib/runner/constants.ts b/packages/plugin-coverage/src/lib/runner/constants.ts index ea888685e..7c65da3df 100644 --- a/packages/plugin-coverage/src/lib/runner/constants.ts +++ b/packages/plugin-coverage/src/lib/runner/constants.ts @@ -1,12 +1 @@ -import path from 'node:path'; -import { pluginWorkDir } from '@code-pushup/utils'; - -export const WORKDIR = pluginWorkDir('coverage'); -export const RUNNER_OUTPUT_PATH = path.join(WORKDIR, 'runner-output.json'); -export const PLUGIN_CONFIG_PATH = path.join( - process.cwd(), - WORKDIR, - 'plugin-config.json', -); - export const INVALID_FUNCTION_NAME = '(empty-report)'; diff --git a/packages/plugin-coverage/src/lib/runner/index.ts b/packages/plugin-coverage/src/lib/runner/index.ts index 89710e224..95710201f 100644 --- a/packages/plugin-coverage/src/lib/runner/index.ts +++ b/packages/plugin-coverage/src/lib/runner/index.ts @@ -1,23 +1,31 @@ import { bold } from 'ansis'; import { writeFile } from 'node:fs/promises'; import path from 'node:path'; -import type { AuditOutputs, RunnerConfig } from '@code-pushup/models'; +import type { + AuditOutputs, + RunnerConfig, + RunnerFilesPaths, +} from '@code-pushup/models'; import { ProcessError, + createRunnerFiles, ensureDirectoryExists, executeProcess, filePathToCliArg, + objectToCliArgs, readJsonFile, ui, } from '@code-pushup/utils'; import type { FinalCoveragePluginConfig } from '../config.js'; import { applyMaxScoreAboveThreshold } from '../utils.js'; -import { PLUGIN_CONFIG_PATH, RUNNER_OUTPUT_PATH } from './constants.js'; import { lcovResultsToAuditOutputs } from './lcov/lcov-runner.js'; -export async function executeRunner(): Promise { +export async function executeRunner({ + runnerConfigPath, + runnerOutputPath, +}: RunnerFilesPaths): Promise { const { reports, coverageToolCommand, coverageTypes } = - await readJsonFile(PLUGIN_CONFIG_PATH); + await readJsonFile(runnerConfigPath); // Run coverage tool if provided if (coverageToolCommand != null) { @@ -41,8 +49,8 @@ export async function executeRunner(): Promise { // Calculate coverage from LCOV results const auditOutputs = await lcovResultsToAuditOutputs(reports, coverageTypes); - await ensureDirectoryExists(path.dirname(RUNNER_OUTPUT_PATH)); - await writeFile(RUNNER_OUTPUT_PATH, JSON.stringify(auditOutputs)); + await ensureDirectoryExists(path.dirname(runnerOutputPath)); + await writeFile(runnerOutputPath, JSON.stringify(auditOutputs)); } export async function createRunnerConfig( @@ -50,15 +58,21 @@ export async function createRunnerConfig( config: FinalCoveragePluginConfig, ): Promise { // Create JSON config for executeRunner - await ensureDirectoryExists(path.dirname(PLUGIN_CONFIG_PATH)); - await writeFile(PLUGIN_CONFIG_PATH, JSON.stringify(config)); + const { runnerConfigPath, runnerOutputPath } = await createRunnerFiles( + 'coverage', + JSON.stringify(config), + ); const threshold = config.perfectScoreThreshold; return { command: 'node', - args: [filePathToCliArg(scriptPath)], - outputFile: RUNNER_OUTPUT_PATH, + args: [ + filePathToCliArg(scriptPath), + ...objectToCliArgs({ runnerConfigPath, runnerOutputPath }), + ], + configFile: runnerConfigPath, + outputFile: runnerOutputPath, ...(threshold != null && { outputTransform: outputs => applyMaxScoreAboveThreshold(outputs as AuditOutputs, threshold), diff --git a/packages/plugin-coverage/src/lib/runner/runner.integration.test.ts b/packages/plugin-coverage/src/lib/runner/runner.integration.test.ts index abe081b2c..814aaa8b9 100644 --- a/packages/plugin-coverage/src/lib/runner/runner.integration.test.ts +++ b/packages/plugin-coverage/src/lib/runner/runner.integration.test.ts @@ -1,19 +1,13 @@ -import { writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { describe, it } from 'vitest'; +import { expect } from 'vitest'; import type { AuditOutput, AuditOutputs, RunnerConfig, } from '@code-pushup/models'; -import { readJsonFile, removeDirectoryIfExists } from '@code-pushup/utils'; +import { createRunnerFiles, readJsonFile } from '@code-pushup/utils'; import type { FinalCoveragePluginConfig } from '../config.js'; -import { - PLUGIN_CONFIG_PATH, - RUNNER_OUTPUT_PATH, - WORKDIR, -} from './constants.js'; import { createRunnerConfig, executeRunner } from './index.js'; describe('createRunnerConfig', () => { @@ -25,15 +19,18 @@ describe('createRunnerConfig', () => { }); expect(runnerConfig).toStrictEqual({ command: 'node', - args: ['"executeRunner.ts"'], + args: [ + '"executeRunner.ts"', + expect.stringContaining('plugin-config.json'), + expect.stringContaining('runner-output.json'), + ], outputTransform: expect.any(Function), outputFile: expect.stringContaining('runner-output.json'), + configFile: expect.stringContaining('plugin-config.json'), }); }); it('should provide plugin config to runner in JSON file', async () => { - await removeDirectoryIfExists(WORKDIR); - const pluginConfig: FinalCoveragePluginConfig = { coverageTypes: ['line'], reports: ['coverage/lcov.info'], @@ -41,10 +38,13 @@ describe('createRunnerConfig', () => { perfectScoreThreshold: 85, }; - await createRunnerConfig('executeRunner.ts', pluginConfig); + const { configFile } = await createRunnerConfig( + 'executeRunner.ts', + pluginConfig, + ); - const config = - await readJsonFile(PLUGIN_CONFIG_PATH); + expect(configFile).toMatch(/.*plugin-config\.json$/); + const config = await readJsonFile(configFile!); expect(config).toStrictEqual(pluginConfig); }); }); @@ -65,10 +65,15 @@ describe('executeRunner', () => { coverageTypes: ['line'], }; - await writeFile(PLUGIN_CONFIG_PATH, JSON.stringify(config)); - await executeRunner(); + const runnerFiles = await createRunnerFiles( + 'coverage', + JSON.stringify(config), + ); + await executeRunner(runnerFiles); - const results = await readJsonFile(RUNNER_OUTPUT_PATH); + const results = await readJsonFile( + runnerFiles.runnerOutputPath, + ); expect(results).toStrictEqual([ expect.objectContaining({ slug: 'line-coverage', diff --git a/packages/plugin-eslint/package.json b/packages/plugin-eslint/package.json index 3cc7dbbe3..e566a0701 100644 --- a/packages/plugin-eslint/package.json +++ b/packages/plugin-eslint/package.json @@ -40,6 +40,7 @@ "dependencies": { "@code-pushup/utils": "0.57.0", "@code-pushup/models": "0.57.0", + "yargs": "^17.7.2", "zod": "^3.22.4" }, "peerDependencies": { diff --git a/packages/plugin-eslint/src/bin.ts b/packages/plugin-eslint/src/bin.ts index bf6572a76..fc625dd73 100644 --- a/packages/plugin-eslint/src/bin.ts +++ b/packages/plugin-eslint/src/bin.ts @@ -1,3 +1,7 @@ +import process from 'node:process'; +import { Parser } from 'yargs/helpers'; import { executeRunner } from './lib/runner/index.js'; -await executeRunner(); +const { runnerConfigPath, runnerOutputPath } = Parser(process.argv); + +await executeRunner({ runnerConfigPath, runnerOutputPath }); diff --git a/packages/plugin-eslint/src/lib/__snapshots__/eslint-plugin.integration.test.ts.snap b/packages/plugin-eslint/src/lib/__snapshots__/eslint-plugin.integration.test.ts.snap index 41d9175c0..388229cba 100644 --- a/packages/plugin-eslint/src/lib/__snapshots__/eslint-plugin.integration.test.ts.snap +++ b/packages/plugin-eslint/src/lib/__snapshots__/eslint-plugin.integration.test.ts.snap @@ -349,9 +349,12 @@ exports[`eslintPlugin > should initialize ESLint plugin for React application 1` "runner": { "args": [ ""/bin.js"", + "--runnerConfigPath="node_modules/.code-pushup/eslint//plugin-config.json"", + "--runnerOutputPath="node_modules/.code-pushup/eslint//runner-output.json"", ], "command": "node", - "outputFile": "node_modules/.code-pushup/eslint/runner-output.json", + "configFile": "node_modules/.code-pushup/eslint//plugin-config.json", + "outputFile": "node_modules/.code-pushup/eslint//runner-output.json", }, "slug": "eslint", "title": "ESLint", diff --git a/packages/plugin-eslint/src/lib/eslint-plugin.integration.test.ts b/packages/plugin-eslint/src/lib/eslint-plugin.integration.test.ts index 34acd8ed3..245611811 100644 --- a/packages/plugin-eslint/src/lib/eslint-plugin.integration.test.ts +++ b/packages/plugin-eslint/src/lib/eslint-plugin.integration.test.ts @@ -1,5 +1,6 @@ import os from 'node:os'; import path from 'node:path'; +import process from 'node:process'; import { fileURLToPath } from 'node:url'; import type { MockInstance } from 'vitest'; import type { Audit, PluginConfig, RunnerConfig } from '@code-pushup/models'; @@ -19,9 +20,19 @@ describe('eslintPlugin', () => { runner: { ...(plugin.runner as RunnerConfig), args: (plugin.runner as RunnerConfig).args?.map(arg => - toUnixPath(arg.replace(path.dirname(thisDir), '')), + toUnixPath(arg.replace(path.dirname(thisDir), '')).replace( + /\/eslint\/\d+\//, + '/eslint//', + ), ), - outputFile: toUnixPath((plugin.runner as RunnerConfig).outputFile), + ...((plugin.runner as RunnerConfig).configFile && { + configFile: toUnixPath( + (plugin.runner as RunnerConfig).configFile!, + ).replace(/\/eslint\/\d+\//, '/eslint//'), + }), + outputFile: toUnixPath( + (plugin.runner as RunnerConfig).outputFile, + ).replace(/\/eslint\/\d+\//, '/eslint//'), }, }); @@ -38,6 +49,7 @@ describe('eslintPlugin', () => { it('should initialize ESLint plugin for React application', async () => { cwdSpy.mockReturnValue(path.join(fixturesDir, 'todos-app')); + const plugin = await eslintPlugin({ eslintrc: 'eslint.config.js', patterns: ['src/**/*.js', 'src/**/*.jsx'], diff --git a/packages/plugin-eslint/src/lib/meta/index.ts b/packages/plugin-eslint/src/lib/meta/index.ts index 58f341908..9303e4ba2 100644 --- a/packages/plugin-eslint/src/lib/meta/index.ts +++ b/packages/plugin-eslint/src/lib/meta/index.ts @@ -13,7 +13,7 @@ export { detectConfigVersion, type ConfigFormat } from './versions/index.js'; export async function listAuditsAndGroups( targets: ESLintTarget[], - customGroups: CustomGroup[] | undefined, + customGroups?: CustomGroup[] | undefined, ): Promise<{ audits: Audit[]; groups: Group[] }> { const rules = await listRules(targets); diff --git a/packages/plugin-eslint/src/lib/runner.integration.test.ts b/packages/plugin-eslint/src/lib/runner.integration.test.ts index 5f78884e5..8a58d60eb 100644 --- a/packages/plugin-eslint/src/lib/runner.integration.test.ts +++ b/packages/plugin-eslint/src/lib/runner.integration.test.ts @@ -2,26 +2,37 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { type MockInstance, describe, expect, it } from 'vitest'; -import type { AuditOutput, AuditOutputs, Issue } from '@code-pushup/models'; +import type { + AuditOutput, + AuditOutputs, + Issue, + RunnerFilesPaths, +} from '@code-pushup/models'; import { osAgnosticAuditOutputs } from '@code-pushup/test-utils'; import { readJsonFile } from '@code-pushup/utils'; import type { ESLintTarget } from './config.js'; import { listAuditsAndGroups } from './meta/index.js'; -import { - RUNNER_OUTPUT_PATH, - createRunnerConfig, - executeRunner, -} from './runner/index.js'; +import { createRunnerConfig, executeRunner } from './runner/index.js'; describe('executeRunner', () => { let cwdSpy: MockInstance<[], string>; let platformSpy: MockInstance<[], NodeJS.Platform>; - const createPluginConfig = async (eslintrc: ESLintTarget['eslintrc']) => { + const createPluginConfig = async ( + eslintrc: ESLintTarget['eslintrc'], + ): Promise => { const patterns = ['src/**/*.js', 'src/**/*.jsx']; const targets: ESLintTarget[] = [{ eslintrc, patterns }]; const { audits } = await listAuditsAndGroups(targets); - await createRunnerConfig('bin.js', audits, targets); + const { outputFile, configFile } = await createRunnerConfig( + 'bin.js', + audits, + targets, + ); + return { + runnerOutputPath: outputFile, + runnerConfigPath: configFile!, + }; }; const appDir = path.join( @@ -45,18 +56,22 @@ describe('executeRunner', () => { }); it('should execute ESLint and create audit results for React application', async () => { - await createPluginConfig('eslint.config.js'); - await executeRunner(); + const runnerPaths = await createPluginConfig('eslint.config.js'); + await executeRunner(runnerPaths); - const json = await readJsonFile(RUNNER_OUTPUT_PATH); + const json = await readJsonFile(runnerPaths.runnerOutputPath); expect(osAgnosticAuditOutputs(json)).toMatchSnapshot(); }); it('should execute runner with custom config using @code-pushup/eslint-config', async () => { - await createPluginConfig('code-pushup.eslint.config.mjs'); - await executeRunner(); + const runnerPaths = await createPluginConfig( + 'code-pushup.eslint.config.mjs', + ); + await executeRunner(runnerPaths); - const json = await readJsonFile(RUNNER_OUTPUT_PATH); + const json = await readJsonFile( + runnerPaths.runnerOutputPath, + ); // expect warnings from unicorn/filename-case rule from default config expect(json).toContainEqual( expect.objectContaining>({ diff --git a/packages/plugin-eslint/src/lib/runner/index.ts b/packages/plugin-eslint/src/lib/runner/index.ts index fc60360e9..e740549ea 100644 --- a/packages/plugin-eslint/src/lib/runner/index.ts +++ b/packages/plugin-eslint/src/lib/runner/index.ts @@ -1,10 +1,16 @@ import { writeFile } from 'node:fs/promises'; import path from 'node:path'; -import type { Audit, AuditOutput, RunnerConfig } from '@code-pushup/models'; +import type { + Audit, + AuditOutput, + RunnerConfig, + RunnerFilesPaths, +} from '@code-pushup/models'; import { + createRunnerFiles, ensureDirectoryExists, filePathToCliArg, - pluginWorkDir, + objectToCliArgs, readJsonFile, } from '@code-pushup/utils'; import type { ESLintPluginRunnerConfig, ESLintTarget } from '../config.js'; @@ -12,17 +18,12 @@ import { lint } from './lint.js'; import { lintResultsToAudits, mergeLinterOutputs } from './transform.js'; import type { LinterOutput } from './types.js'; -export const WORKDIR = pluginWorkDir('eslint'); -export const RUNNER_OUTPUT_PATH = path.join(WORKDIR, 'runner-output.json'); -export const PLUGIN_CONFIG_PATH = path.join( - process.cwd(), - WORKDIR, - 'plugin-config.json', -); - -export async function executeRunner(): Promise { +export async function executeRunner({ + runnerConfigPath, + runnerOutputPath, +}: RunnerFilesPaths): Promise { const { slugs, targets } = - await readJsonFile(PLUGIN_CONFIG_PATH); + await readJsonFile(runnerConfigPath); const linterOutputs = await targets.reduce( async (acc, target) => [...(await acc), await lint(target)], @@ -42,8 +43,8 @@ export async function executeRunner(): Promise { }, ); - await ensureDirectoryExists(path.dirname(RUNNER_OUTPUT_PATH)); - await writeFile(RUNNER_OUTPUT_PATH, JSON.stringify(audits)); + await ensureDirectoryExists(path.dirname(runnerOutputPath)); + await writeFile(runnerOutputPath, JSON.stringify(audits)); } export async function createRunnerConfig( @@ -55,12 +56,18 @@ export async function createRunnerConfig( targets, slugs: audits.map(audit => audit.slug), }; - await ensureDirectoryExists(path.dirname(PLUGIN_CONFIG_PATH)); - await writeFile(PLUGIN_CONFIG_PATH, JSON.stringify(config)); + const { runnerConfigPath, runnerOutputPath } = await createRunnerFiles( + 'eslint', + JSON.stringify(config), + ); return { command: 'node', - args: [filePathToCliArg(scriptPath)], - outputFile: RUNNER_OUTPUT_PATH, + args: [ + filePathToCliArg(scriptPath), + ...objectToCliArgs({ runnerConfigPath, runnerOutputPath }), + ], + configFile: runnerConfigPath, + outputFile: runnerOutputPath, }; } diff --git a/packages/plugin-js-packages/package.json b/packages/plugin-js-packages/package.json index 9164e132c..ebe4eda8f 100644 --- a/packages/plugin-js-packages/package.json +++ b/packages/plugin-js-packages/package.json @@ -41,6 +41,7 @@ "@code-pushup/utils": "0.57.0", "build-md": "^0.4.1", "semver": "^7.6.0", + "yargs": "^17.7.2", "zod": "^3.22.4" } } diff --git a/packages/plugin-js-packages/src/bin.ts b/packages/plugin-js-packages/src/bin.ts index bf6572a76..fc625dd73 100644 --- a/packages/plugin-js-packages/src/bin.ts +++ b/packages/plugin-js-packages/src/bin.ts @@ -1,3 +1,7 @@ +import process from 'node:process'; +import { Parser } from 'yargs/helpers'; import { executeRunner } from './lib/runner/index.js'; -await executeRunner(); +const { runnerConfigPath, runnerOutputPath } = Parser(process.argv); + +await executeRunner({ runnerConfigPath, runnerOutputPath }); diff --git a/packages/plugin-js-packages/src/lib/runner/constants.ts b/packages/plugin-js-packages/src/lib/runner/constants.ts deleted file mode 100644 index 19d0f6f7d..000000000 --- a/packages/plugin-js-packages/src/lib/runner/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -import path from 'node:path'; -import { pluginWorkDir } from '@code-pushup/utils'; - -export const WORKDIR = pluginWorkDir('js-packages'); -export const RUNNER_OUTPUT_PATH = path.join(WORKDIR, 'runner-output.json'); -export const PLUGIN_CONFIG_PATH = path.join( - process.cwd(), - WORKDIR, - 'plugin-config.json', -); diff --git a/packages/plugin-js-packages/src/lib/runner/index.ts b/packages/plugin-js-packages/src/lib/runner/index.ts index 2c5f9a9e7..e2784e30e 100644 --- a/packages/plugin-js-packages/src/lib/runner/index.ts +++ b/packages/plugin-js-packages/src/lib/runner/index.ts @@ -1,13 +1,15 @@ import { writeFile } from 'node:fs/promises'; import path from 'node:path'; -import type { RunnerConfig } from '@code-pushup/models'; +import type { RunnerConfig, RunnerFilesPaths } from '@code-pushup/models'; import { + createRunnerFiles, ensureDirectoryExists, executeProcess, filePathToCliArg, isPromiseFulfilledResult, isPromiseRejectedResult, objectFromEntries, + objectToCliArgs, readJsonFile, } from '@code-pushup/utils'; import { @@ -22,7 +24,6 @@ import { dependencyGroupToLong } from '../constants.js'; import { packageManagers } from '../package-managers/package-managers.js'; import { auditResultToAuditOutput } from './audit/transform.js'; import type { AuditResult } from './audit/types.js'; -import { PLUGIN_CONFIG_PATH, RUNNER_OUTPUT_PATH } from './constants.js'; import { outdatedResultToAuditOutput } from './outdated/transform.js'; import { findAllPackageJson, getTotalDependencies } from './utils.js'; @@ -30,24 +31,33 @@ export async function createRunnerConfig( scriptPath: string, config: FinalJSPackagesPluginConfig, ): Promise { - await ensureDirectoryExists(path.dirname(PLUGIN_CONFIG_PATH)); - await writeFile(PLUGIN_CONFIG_PATH, JSON.stringify(config)); + const { runnerConfigPath, runnerOutputPath } = await createRunnerFiles( + 'js-packages', + JSON.stringify(config), + ); return { command: 'node', - args: [filePathToCliArg(scriptPath)], - outputFile: RUNNER_OUTPUT_PATH, + args: [ + filePathToCliArg(scriptPath), + ...objectToCliArgs({ runnerConfigPath, runnerOutputPath }), + ], + configFile: runnerConfigPath, + outputFile: runnerOutputPath, }; } -export async function executeRunner(): Promise { +export async function executeRunner({ + runnerConfigPath, + runnerOutputPath, +}: RunnerFilesPaths): Promise { const { packageManager, checks, auditLevelMapping, packageJsonPaths, dependencyGroups: depGroups, - } = await readJsonFile(PLUGIN_CONFIG_PATH); + } = await readJsonFile(runnerConfigPath); const auditResults = checks.includes('audit') ? await processAudit(packageManager, depGroups, auditLevelMapping) @@ -58,8 +68,8 @@ export async function executeRunner(): Promise { : []; const checkResults = [...auditResults, ...outdatedResults]; - await ensureDirectoryExists(path.dirname(RUNNER_OUTPUT_PATH)); - await writeFile(RUNNER_OUTPUT_PATH, JSON.stringify(checkResults)); + await ensureDirectoryExists(path.dirname(runnerOutputPath)); + await writeFile(runnerOutputPath, JSON.stringify(checkResults)); } async function processOutdated( diff --git a/packages/plugin-js-packages/src/lib/runner/runner.integration.test.ts b/packages/plugin-js-packages/src/lib/runner/runner.integration.test.ts index 9e648923d..bff5912e3 100644 --- a/packages/plugin-js-packages/src/lib/runner/runner.integration.test.ts +++ b/packages/plugin-js-packages/src/lib/runner/runner.integration.test.ts @@ -1,9 +1,8 @@ import { describe, expect, it } from 'vitest'; import type { RunnerConfig } from '@code-pushup/models'; -import { readJsonFile, removeDirectoryIfExists } from '@code-pushup/utils'; +import { readJsonFile } from '@code-pushup/utils'; import type { FinalJSPackagesPluginConfig } from '../config.js'; import { defaultAuditLevelMapping } from '../constants.js'; -import { PLUGIN_CONFIG_PATH, WORKDIR } from './constants.js'; import { createRunnerConfig } from './index.js'; describe('createRunnerConfig', () => { @@ -17,13 +16,17 @@ describe('createRunnerConfig', () => { }); expect(runnerConfig).toStrictEqual({ command: 'node', - args: ['"executeRunner.ts"'], + args: [ + '"executeRunner.ts"', + expect.stringContaining('plugin-config.json'), + expect.stringContaining('runner-output.json'), + ], outputFile: expect.stringContaining('runner-output.json'), + configFile: expect.stringContaining('plugin-config.json'), }); }); it('should provide plugin config to runner in JSON file', async () => { - await removeDirectoryIfExists(WORKDIR); const pluginConfig: FinalJSPackagesPluginConfig = { packageManager: 'yarn-classic', checks: ['outdated'], @@ -31,9 +34,12 @@ describe('createRunnerConfig', () => { auditLevelMapping: { ...defaultAuditLevelMapping, moderate: 'error' }, packageJsonPaths: ['package.json'], }; - await createRunnerConfig('executeRunner.ts', pluginConfig); - const config = - await readJsonFile(PLUGIN_CONFIG_PATH); + const { configFile } = await createRunnerConfig( + 'executeRunner.ts', + pluginConfig, + ); + expect(configFile).toMatch(/.*plugin-config\.json$/); + const config = await readJsonFile(configFile!); expect(config).toStrictEqual(pluginConfig); }); }); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 7c8fd8556..8c91a2ebf 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -124,3 +124,4 @@ export type { } from './lib/types.js'; export { verboseUtils } from './lib/verbose-utils.js'; export { parseSchema, SchemaValidationError } from './lib/zod-validation.js'; +export { createRunnerFiles } from './lib/create-runner-files.js'; diff --git a/packages/utils/src/lib/create-runner-files.ts b/packages/utils/src/lib/create-runner-files.ts new file mode 100644 index 000000000..db0085a5c --- /dev/null +++ b/packages/utils/src/lib/create-runner-files.ts @@ -0,0 +1,28 @@ +import { writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import type { RunnerFilesPaths } from '@code-pushup/models'; +import { ensureDirectoryExists, pluginWorkDir } from './file-system.js'; + +/** + * Function to create timestamp nested plugin runner files for config and output. + * + * @param pluginSlug - slug of the plugin name used as folder names to group same plugin configs + * @param configJSON - config of the plugin runner as JSON. + */ +export async function createRunnerFiles( + pluginSlug: string, + configJSON: string, +): Promise { + const timestamp = Date.now().toString(); + const runnerWorkDir = path.join(pluginWorkDir(pluginSlug), timestamp); + const runnerConfigPath = path.join(runnerWorkDir, 'plugin-config.json'); + const runnerOutputPath = path.join(runnerWorkDir, 'runner-output.json'); + + await ensureDirectoryExists(path.dirname(runnerOutputPath)); + await writeFile(runnerConfigPath, configJSON); + + return { + runnerConfigPath, + runnerOutputPath, + }; +} diff --git a/packages/utils/src/lib/transform.ts b/packages/utils/src/lib/transform.ts index d3943a0c4..9224085bb 100644 --- a/packages/utils/src/lib/transform.ts +++ b/packages/utils/src/lib/transform.ts @@ -54,7 +54,7 @@ export type CliArgsObject> = * Converts an object with different types of values into an array of command-line arguments. * * @example - * const args = objectToProcessArgs({ + * const args = objectToCliArgs({ * _: ['node', 'index.js'], // node index.js * name: 'Juanita', // --name=Juanita * formats: ['json', 'md'] // --format=json --format=md From 9cad03a4eda563c63f598d59071e321226a92faf Mon Sep 17 00:00:00 2001 From: Vojtech Masek Date: Tue, 4 Feb 2025 00:33:15 +0100 Subject: [PATCH 2/4] build: run code pushup with no progress and verbose --- project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.json b/project.json index 8745d84e2..a4c550bfb 100644 --- a/project.json +++ b/project.json @@ -5,7 +5,7 @@ "code-pushup": { "executor": "nx:run-commands", "options": { - "command": "node packages/cli/src/index.ts", + "command": "node packages/cli/src/index.ts --no-progress --verbose", "env": { "NODE_OPTIONS": "--import tsx", "TSX_TSCONFIG_PATH": "tsconfig.base.json" From 2c5af87dcfa576014460aca526c831383730634c Mon Sep 17 00:00:00 2001 From: Vojtech Masek Date: Tue, 4 Feb 2025 01:17:43 +0100 Subject: [PATCH 3/4] refactor: add various logs --- packages/core/src/lib/collect-and-persist.ts | 1 + packages/plugin-eslint/src/lib/runner/index.ts | 3 +++ packages/utils/src/lib/execute-process.ts | 9 +++++++++ 3 files changed, 13 insertions(+) diff --git a/packages/core/src/lib/collect-and-persist.ts b/packages/core/src/lib/collect-and-persist.ts index 21c379695..f7aaeda49 100644 --- a/packages/core/src/lib/collect-and-persist.ts +++ b/packages/core/src/lib/collect-and-persist.ts @@ -28,6 +28,7 @@ export async function collectAndPersistReports( const report = await collect(options); const sortedScoredReport = sortReport(scoreReport(report)); + const persistResults = await persistReport( report, sortedScoredReport, diff --git a/packages/plugin-eslint/src/lib/runner/index.ts b/packages/plugin-eslint/src/lib/runner/index.ts index e740549ea..b18967e53 100644 --- a/packages/plugin-eslint/src/lib/runner/index.ts +++ b/packages/plugin-eslint/src/lib/runner/index.ts @@ -12,6 +12,7 @@ import { filePathToCliArg, objectToCliArgs, readJsonFile, + ui, } from '@code-pushup/utils'; import type { ESLintPluginRunnerConfig, ESLintTarget } from '../config.js'; import { lint } from './lint.js'; @@ -25,6 +26,8 @@ export async function executeRunner({ const { slugs, targets } = await readJsonFile(runnerConfigPath); + ui().logger.log(`ESLint plugin executing ${targets.length} lint targets`); + const linterOutputs = await targets.reduce( async (acc, target) => [...(await acc), await lint(target)], Promise.resolve([]), diff --git a/packages/utils/src/lib/execute-process.ts b/packages/utils/src/lib/execute-process.ts index e561a18a0..8c506c15e 100644 --- a/packages/utils/src/lib/execute-process.ts +++ b/packages/utils/src/lib/execute-process.ts @@ -1,3 +1,4 @@ +import { gray } from 'ansis'; import { type ChildProcess, type ChildProcessByStdio, @@ -6,6 +7,7 @@ import { spawn, } from 'node:child_process'; import type { Readable, Writable } from 'node:stream'; +import { ui } from './logging.js'; import { calcDuration } from './reports/utils.js'; /** @@ -148,6 +150,13 @@ export function executeProcess(cfg: ProcessConfig): Promise { const date = new Date().toISOString(); const start = performance.now(); + const logCommand = [command, ...(args || [])].join(' '); + ui().logger.log( + gray( + `Executing command:\n${logCommand}\nIn working directory:\n${cfg.cwd}`, + ), + ); + return new Promise((resolve, reject) => { // shell:true tells Windows to use shell command for spawning a child process const spawnedProcess = spawn(command, args ?? [], { From 3a50ad5acd827649aa3d92bdbaa5e9f0a42f6f6d Mon Sep 17 00:00:00 2001 From: Vojtech Masek Date: Mon, 10 Feb 2025 22:39:41 +0100 Subject: [PATCH 4/4] test(plugin-eslint): skip nx integration tests on windows context: https://github.com/nrwl/nx/issues/27494#issuecomment-2633836688 --- packages/plugin-eslint/src/lib/nx.integration.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/plugin-eslint/src/lib/nx.integration.test.ts b/packages/plugin-eslint/src/lib/nx.integration.test.ts index 0874b39d5..9ac98ee4c 100644 --- a/packages/plugin-eslint/src/lib/nx.integration.test.ts +++ b/packages/plugin-eslint/src/lib/nx.integration.test.ts @@ -1,4 +1,5 @@ import path from 'node:path'; +import process from 'node:process'; import { fileURLToPath } from 'node:url'; import type { MockInstance } from 'vitest'; import { executeProcess } from '@code-pushup/utils'; @@ -11,7 +12,8 @@ import { type Project = 'cli' | 'core' | 'nx-plugin' | 'utils'; -describe('Nx helpers', () => { +// skipping tests on Windows due to a problem with createProjectGraphAsync that hangs forever, issue seems to be connected to nested git or some other Nx graph related problem https://github.com/nrwl/nx/issues/27494#issuecomment-2633836688 +describe.skipIf(process.platform === 'win32')('Nx helpers', () => { let cwdSpy: MockInstance<[], string>; beforeAll(async () => {