From 9b26aac89685cdd8c3cc26e109df42306f71bcd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Wed, 26 Nov 2025 15:16:49 +0100 Subject: [PATCH 01/16] style: reduce line count of named inputs config --- nx.json | 43 ++++++++----------------------------------- 1 file changed, 8 insertions(+), 35 deletions(-) diff --git a/nx.json b/nx.json index 80aa82594..c6ab78125 100644 --- a/nx.json +++ b/nx.json @@ -2,11 +2,7 @@ "$schema": "./node_modules/nx/schemas/nx-schema.json", "namedInputs": { "default": ["{projectRoot}/**/*", "sharedGlobals"], - "os": [ - { - "runtime": "node -e \"console.log(require('os').platform())\"" - } - ], + "os": [{ "runtime": "node -e \"console.log(require('os').platform())\"" }], "production": [ "default", "!{projectRoot}/README.md", @@ -28,39 +24,16 @@ ], "test-vitest-inputs": [ "os", - { - "env": "NX_VERBOSE_LOGGING" - }, - { - "externalDependencies": ["vitest"] - } - ], - "lint-eslint-inputs": [ - { - "externalDependencies": ["eslint"] - } - ], - "typecheck-typescript-inputs": [ - { - "externalDependencies": ["typescript"] - } + { "env": "NX_VERBOSE_LOGGING" }, + { "externalDependencies": ["vitest"] } ], + "lint-eslint-inputs": [{ "externalDependencies": ["eslint"] }], + "typecheck-typescript-inputs": [{ "externalDependencies": ["typescript"] }], "code-pushup-inputs": [ - { - "env": "NODE_OPTIONS" - }, - { - "env": "TSX_TSCONFIG_PATH" - } + { "env": "NODE_OPTIONS" }, + { "env": "TSX_TSCONFIG_PATH" } ], - "sharedGlobals": [ - { - "runtime": "node -v" - }, - { - "runtime": "npm -v" - } - ] + "sharedGlobals": [{ "runtime": "node -v" }, { "runtime": "npm -v" }] }, "targetDefaults": { "lint": { From 05e8c3914d53108cb229448a4089ff0ea8995cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Wed, 26 Nov 2025 15:24:06 +0100 Subject: [PATCH 02/16] chore: remove unused cache outputs configuration --- nx.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/nx.json b/nx.json index c6ab78125..87a0886cb 100644 --- a/nx.json +++ b/nx.json @@ -115,10 +115,6 @@ }, "code-pushup": { "cache": false, - "outputs": [ - "{projectRoot}/.code-pushup/report.md", - "{projectRoot}/.code-pushup/report.json" - ], "executor": "nx:run-commands", "options": { "command": "node packages/cli/src/index.ts", From 924cc1f729c57018eebc137b4b1d84571592ef00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Wed, 26 Nov 2025 15:26:13 +0100 Subject: [PATCH 03/16] ci: remove --verbose arg from code-pushup targets --- nx.json | 8 -------- project.json | 1 - 2 files changed, 9 deletions(-) diff --git a/nx.json b/nx.json index 87a0886cb..55f148281 100644 --- a/nx.json +++ b/nx.json @@ -119,7 +119,6 @@ "options": { "command": "node packages/cli/src/index.ts", "args": [ - "--verbose", "--config={projectRoot}/code-pushup.config.ts", "--cache.read", "--persist.outputDir={projectRoot}/.code-pushup", @@ -140,7 +139,6 @@ "options": { "command": "node packages/cli/src/index.ts collect", "args": [ - "--verbose", "--config={projectRoot}/code-pushup.config.ts", "--cache.write", "--onlyPlugins=coverage", @@ -161,7 +159,6 @@ "options": { "command": "node packages/cli/src/index.ts collect", "args": [ - "--verbose", "--config={projectRoot}/code-pushup.config.ts", "--cache.write", "--onlyPlugins=eslint", @@ -186,7 +183,6 @@ "options": { "command": "node packages/cli/src/index.ts collect", "args": [ - "--verbose", "--config={projectRoot}/code-pushup.config.ts", "--onlyPlugins=js-packages", "--persist.outputDir={projectRoot}/.code-pushup" @@ -205,7 +201,6 @@ "options": { "command": "node packages/cli/src/index.ts collect", "args": [ - "--verbose", "--config={projectRoot}/code-pushup.config.ts", "--cache.write", "--onlyPlugins=lighthouse", @@ -230,7 +225,6 @@ "options": { "command": "node packages/cli/src/index.ts collect", "args": [ - "--verbose", "--config={projectRoot}/code-pushup.config.ts", "--cache.write", "--onlyPlugins=jsdocs", @@ -255,7 +249,6 @@ "options": { "command": "node packages/cli/src/index.ts collect", "args": [ - "--verbose", "--config={projectRoot}/code-pushup.config.ts", "--cache.write", "--onlyPlugins=typescript", @@ -276,7 +269,6 @@ "options": { "command": "node packages/cli/src/index.ts collect", "args": [ - "--verbose", "--config={projectRoot}/code-pushup.config.ts", "--cache.write", "--onlyPlugins=axe", diff --git a/project.json b/project.json index 2b378ab31..62567b891 100644 --- a/project.json +++ b/project.json @@ -32,7 +32,6 @@ "executor": "nx:run-commands", "options": { "args": [ - "--verbose", "--cache.read", "--persist.outputDir={projectRoot}/.code-pushup", "--upload.project={projectName}" From 3a29eb1061fd7ec83f21a8cbf23a1e2a86ac5045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Wed, 26 Nov 2025 15:57:17 +0100 Subject: [PATCH 04/16] ci: simplify standalone code-pushup target --- code-pushup.preset.ts | 57 +++++++++++++++++++++++++------------------ nx.json | 1 + project.json | 34 ++------------------------ 3 files changed, 36 insertions(+), 56 deletions(-) diff --git a/code-pushup.preset.ts b/code-pushup.preset.ts index fda87e20c..03a4cf03a 100644 --- a/code-pushup.preset.ts +++ b/code-pushup.preset.ts @@ -6,6 +6,7 @@ import type { } from './packages/models/src/index.js'; import axePlugin from './packages/plugin-axe/src/index.js'; import coveragePlugin, { + type CoveragePluginConfig, getNxCoveragePaths, } from './packages/plugin-coverage/src/index.js'; import eslintPlugin, { @@ -174,17 +175,22 @@ export const eslintCoreConfigNx = async ( ): Promise => ({ plugins: [ projectName - ? await eslintPlugin({ - eslintrc: `packages/${projectName}/eslint.config.js`, - patterns: ['.'], - }) - : await eslintPlugin(await eslintConfigFromAllNxProjects(), { - artifacts: { - // We leverage Nx dependsOn to only run all lint targets before we run code-pushup - // generateArtifactsCommand: 'npx nx run-many -t lint', - artifactsPaths: ['packages/**/.eslint/eslint-report.json'], + ? await eslintPlugin( + { + eslintrc: `packages/${projectName}/eslint.config.js`, + patterns: ['.'], }, - }), + { + artifacts: { + // We leverage Nx dependsOn to only run all lint targets before we run code-pushup + // generateArtifactsCommand: 'npx nx run-many -t lint', + artifactsPaths: [ + `packages/${projectName}/.eslint/eslint-report.json`, + ], + }, + }, + ) + : await eslintPlugin(await eslintConfigFromAllNxProjects()), ], categories: eslintCategories, }); @@ -199,21 +205,24 @@ export const typescriptPluginConfig = async ( export const coverageCoreConfigNx = async ( projectName?: string, ): Promise => { - const targetNames = ['unit-test', 'int-test']; + const config: CoveragePluginConfig = projectName + ? // We do not need to run a coverageToolCommand. This is handled over the Nx task graph. + { + reports: [ + { + pathToProject: `packages/${projectName}`, + resultsPath: `packages/${projectName}/coverage/lcov.info`, + }, + ], + } + : { + reports: await getNxCoveragePaths(['unit-test', 'int-test']), + coverageToolCommand: { + command: 'npx nx run-many -t unit-test,int-test', + }, + }; return { - plugins: [ - await coveragePlugin({ - // We do not need to run a coverageToolCommand. This is handled over the Nx task graph. - reports: projectName - ? [ - { - pathToProject: `packages/${projectName}`, - resultsPath: `packages/${projectName}/coverage/lcov.info`, - }, - ] - : await getNxCoveragePaths(targetNames), - }), - ], + plugins: [await coveragePlugin(config)], categories: coverageCategories, }; }; diff --git a/nx.json b/nx.json index 55f148281..df00ad553 100644 --- a/nx.json +++ b/nx.json @@ -116,6 +116,7 @@ "code-pushup": { "cache": false, "executor": "nx:run-commands", + "dependsOn": ["code-pushup-*"], "options": { "command": "node packages/cli/src/index.ts", "args": [ diff --git a/project.json b/project.json index 62567b891..bde78f599 100644 --- a/project.json +++ b/project.json @@ -2,40 +2,10 @@ "name": "cli-workspace", "$schema": "node_modules/nx/schemas/project-schema.json", "targets": { - "code-pushup-js-packages": {}, - "code-pushup-lighthouse": {}, - "code-pushup-coverage": { - "dependsOn": [ - { - "target": "unit-test", - "projects": "*" - }, - { - "target": "int-test", - "projects": "*" - } - ] - }, - "code-pushup-eslint": { - "dependsOn": [ - { - "target": "lint", - "projects": "*" - } - ] - }, - "code-pushup-jsdocs": {}, - "code-pushup-typescript": {}, - "code-pushup-axe": {}, "code-pushup": { - "dependsOn": ["code-pushup-*"], - "executor": "nx:run-commands", + "dependsOn": [], "options": { - "args": [ - "--cache.read", - "--persist.outputDir={projectRoot}/.code-pushup", - "--upload.project={projectName}" - ] + "args": [] } } } From 5d9577305ac707eb697a3bee367bc7a0a67a3cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Wed, 26 Nov 2025 16:19:00 +0100 Subject: [PATCH 05/16] feat(plugin-typescript): make init function synchronous --- .../default-setup/code-pushup.config.ts | 2 +- packages/plugin-typescript/README.md | 30 +++++++++---------- .../src/lib/typescript-plugin.ts | 4 +-- .../src/lib/typescript-plugin.unit.test.ts | 24 +++++++-------- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/code-pushup.config.ts b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/code-pushup.config.ts index 1f10c7b15..7bb8026d1 100644 --- a/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/code-pushup.config.ts +++ b/e2e/plugin-typescript-e2e/mocks/fixtures/default-setup/code-pushup.config.ts @@ -4,7 +4,7 @@ import typescriptPlugin, { } from '@code-pushup/typescript-plugin'; export default { - plugins: [await typescriptPlugin()], + plugins: [typescriptPlugin()], categories: [ { slug: 'type-safety', diff --git a/packages/plugin-typescript/README.md b/packages/plugin-typescript/README.md index 7f2d5da6b..888092567 100644 --- a/packages/plugin-typescript/README.md +++ b/packages/plugin-typescript/README.md @@ -39,19 +39,19 @@ TypeScript compiler diagnostics are mapped to Code PushUp audits in the followin 3. Add this plugin to the `plugins` array in your Code PushUp CLI config file (e.g. `code-pushup.config.ts`). -By default, a root `tsconfig.json` is used to compile your codebase. Based on those compiler options, the plugin will generate audits. - -```ts -import typescriptPlugin from '@code-pushup/typescript-plugin'; - -export default { - // ... - plugins: [ - // ... - await typescriptPlugin(), - ], -}; -``` + By default, a root `tsconfig.json` is used to compile your codebase. Based on those compiler options, the plugin will generate audits. + + ```ts + import typescriptPlugin from '@code-pushup/typescript-plugin'; + + export default { + // ... + plugins: [ + // ... + typescriptPlugin(), + ], + }; + ``` 4. Run the CLI with `npx code-pushup collect` and view or upload the report (refer to [CLI docs](../cli/README.md)). @@ -94,7 +94,7 @@ The plugin accepts the following parameters: Optional parameter. The `tsconfig` option accepts a string that defines the path to your config file and defaults to `tsconfig.json`. ```js -await typescriptPlugin({ +typescriptPlugin({ tsconfig: './tsconfig.json', }); ``` @@ -104,7 +104,7 @@ await typescriptPlugin({ The `onlyAudits` option allows you to specify which documentation types you want to measure. Only the specified audits will be included in the results. All audits are included by default. Example: ```js -await typescriptPlugin({ +typescriptPlugin({ onlyAudits: ['no-implicit-any'], }); ``` diff --git a/packages/plugin-typescript/src/lib/typescript-plugin.ts b/packages/plugin-typescript/src/lib/typescript-plugin.ts index 48fa8de2d..0f2fc85cc 100644 --- a/packages/plugin-typescript/src/lib/typescript-plugin.ts +++ b/packages/plugin-typescript/src/lib/typescript-plugin.ts @@ -14,9 +14,9 @@ const packageJson = createRequire(import.meta.url)( '../../package.json', ) as typeof import('../../package.json'); -export async function typescriptPlugin( +export function typescriptPlugin( options?: TypescriptPluginOptions, -): Promise { +): PluginConfig { const { tsconfig = DEFAULT_TS_CONFIG, onlyAudits, diff --git a/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts b/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts index 1d9ae3365..a01ba8f44 100644 --- a/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts +++ b/packages/plugin-typescript/src/lib/typescript-plugin.unit.test.ts @@ -2,12 +2,11 @@ import ansis from 'ansis'; import { expect } from 'vitest'; import { pluginConfigSchema } from '@code-pushup/models'; import { AUDITS, GROUPS } from './constants.js'; -import type { TypescriptPluginOptions } from './schema.js'; import { typescriptPlugin } from './typescript-plugin.js'; -describe('typescriptPlugin-config-object', () => { - it('should create valid plugin config without options', async () => { - const pluginConfig = await typescriptPlugin(); +describe('typescriptPlugin', () => { + it('should create valid plugin config without options', () => { + const pluginConfig = typescriptPlugin(); expect(() => pluginConfigSchema.parse(pluginConfig)).not.toThrow(); @@ -17,8 +16,8 @@ describe('typescriptPlugin-config-object', () => { expect(groups!).toHaveLength(GROUPS.length); }); - it('should create valid plugin config', async () => { - const pluginConfig = await typescriptPlugin({ + it('should create valid plugin config', () => { + const pluginConfig = typescriptPlugin({ tsconfig: 'mocked-away/tsconfig.json', onlyAudits: ['syntax-errors', 'semantic-errors', 'configuration-errors'], }); @@ -31,21 +30,22 @@ describe('typescriptPlugin-config-object', () => { expect(groups!).toHaveLength(2); }); - it('should throw for invalid valid params', async () => { - await expect(() => + it('should throw for invalid valid params', () => { + expect(() => typescriptPlugin({ + // @ts-expect-error testing invalid argument type tsconfig: 42, - } as unknown as TypescriptPluginOptions), - ).rejects + }), + ) .toThrow(`Error parsing TypeScript Plugin options: SchemaValidationError: Invalid ${ansis.bold('TypescriptPluginConfig')} ✖ Invalid input: expected string, received number → at tsconfig `); }); - it('should pass scoreTargets to PluginConfig when provided', async () => { + it('should pass scoreTargets to PluginConfig when provided', () => { const scoreTargets = { 'no-implicit-any-errors': 0.9 }; - const pluginConfig = await typescriptPlugin({ + const pluginConfig = typescriptPlugin({ scoreTargets, }); From 043eb80e425d273797541accb76210a625b0804d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Wed, 26 Nov 2025 16:48:33 +0100 Subject: [PATCH 06/16] chore: clean up code-pushup presets and prepare for reuse on project-level --- code-pushup.config.ts | 59 +++---- code-pushup.preset.ts | 346 +++++++++++++++++++++--------------------- 2 files changed, 190 insertions(+), 215 deletions(-) diff --git a/code-pushup.config.ts b/code-pushup.config.ts index a80c0ce23..9629a581d 100644 --- a/code-pushup.config.ts +++ b/code-pushup.config.ts @@ -1,50 +1,27 @@ import 'dotenv/config'; import { axeCoreConfig, - coverageCoreConfigNx, - eslintCoreConfigNx, - jsDocsCoreConfig, - jsPackagesCoreConfig, - lighthouseCoreConfig, - typescriptPluginConfig, + configureCoveragePlugin, + configureEslintPlugin, + configureJsDocsPlugin, + configureJsPackagesPlugin, + configureLighthousePlugin, + configureTypescriptPlugin, + configureUpload, } from './code-pushup.preset.js'; -import type { CoreConfig } from './packages/models/src/index.js'; import { mergeConfigs } from './packages/utils/src/index.js'; -const project = process.env['NX_TASK_TARGET_PROJECT'] || 'cli-workspace'; - -const config: CoreConfig = { - ...(process.env['CP_API_KEY'] && { - upload: { - project, - organization: 'code-pushup', - server: 'https://api.staging.code-pushup.dev/graphql', - apiKey: process.env['CP_API_KEY'], - }, - }), - plugins: [], -}; +// TODO: replace with something meaningful, or move out of the repo +const TARGET_URL = + 'https://github.com/code-pushup/cli?tab=readme-ov-file#code-pushup-cli/'; export default mergeConfigs( - config, - await coverageCoreConfigNx(), - await jsPackagesCoreConfig(), - await lighthouseCoreConfig( - 'https://github.com/code-pushup/cli?tab=readme-ov-file#code-pushup-cli/', - ), - await typescriptPluginConfig({ - tsconfig: 'packages/cli/tsconfig.lib.json', - }), - await eslintCoreConfigNx(), - jsDocsCoreConfig([ - 'packages/**/src/**/*.ts', - '!packages/**/node_modules', - '!packages/**/{mocks,mock}', - '!**/*.{spec,test}.ts', - '!**/implementation/**', - '!**/internal/**', - ]), - axeCoreConfig( - 'https://github.com/code-pushup/cli?tab=readme-ov-file#code-pushup-cli/', - ), + configureUpload(), + await configureEslintPlugin(), + await configureCoveragePlugin(), + await configureJsPackagesPlugin(), + configureTypescriptPlugin(), + configureJsDocsPlugin(), + await configureLighthousePlugin(TARGET_URL), + axeCoreConfig(TARGET_URL), ); diff --git a/code-pushup.preset.ts b/code-pushup.preset.ts index 03a4cf03a..77e2003f0 100644 --- a/code-pushup.preset.ts +++ b/code-pushup.preset.ts @@ -12,199 +12,83 @@ import coveragePlugin, { import eslintPlugin, { eslintConfigFromAllNxProjects, } from './packages/plugin-eslint/src/index.js'; -import type { ESLintTarget } from './packages/plugin-eslint/src/lib/config.js'; -import { nxProjectsToConfig } from './packages/plugin-eslint/src/lib/nx/projects-to-config.js'; import jsPackagesPlugin from './packages/plugin-js-packages/src/index.js'; import jsDocsPlugin from './packages/plugin-jsdocs/src/index.js'; -import type { JsDocsPluginTransformedConfig } from './packages/plugin-jsdocs/src/lib/config.js'; import { PLUGIN_SLUG, groups, } from './packages/plugin-jsdocs/src/lib/constants.js'; -import { filterGroupsByOnlyAudits } from './packages/plugin-jsdocs/src/lib/utils.js'; -import lighthousePlugin, { +import { lighthouseGroupRef, + lighthousePlugin, mergeLighthouseCategories, } from './packages/plugin-lighthouse/src/index.js'; import typescriptPlugin, { - type TypescriptPluginOptions, getCategories, } from './packages/plugin-typescript/src/index.js'; -export const jsPackagesCategories: CategoryConfig[] = [ - { - slug: 'security', - title: 'Security', - description: 'Finds known **vulnerabilities** in 3rd-party packages.', - refs: [ - { - type: 'group', - plugin: 'js-packages', - slug: 'npm-audit', - weight: 1, +export function configureUpload(): CoreConfig { + return { + ...(process.env['CP_API_KEY'] && { + upload: { + server: 'https://api.staging.code-pushup.dev/graphql', + apiKey: process.env['CP_API_KEY'], + organization: 'code-pushup', + project: process.env['NX_TASK_TARGET_PROJECT'] ?? 'cli-workspace', }, + }), + plugins: [], + }; +} + +export async function configureEslintPlugin( + projectName?: string, +): Promise { + return { + plugins: [ + projectName + ? await eslintPlugin( + { + eslintrc: `packages/${projectName}/eslint.config.js`, + patterns: ['.'], + }, + { + artifacts: { + // We leverage Nx dependsOn to only run all lint targets before we run code-pushup + // generateArtifactsCommand: 'npx nx run-many -t lint', + artifactsPaths: [ + `packages/${projectName}/.eslint/eslint-report.json`, + ], + }, + }, + ) + : await eslintPlugin(await eslintConfigFromAllNxProjects()), ], - }, - { - slug: 'updates', - title: 'Updates', - description: 'Finds **outdated** 3rd-party packages.', - refs: [ + categories: [ { - type: 'group', - plugin: 'js-packages', - slug: 'npm-outdated', - weight: 1, + slug: 'bug-prevention', + title: 'Bug prevention', + description: 'Lint rules that find **potential bugs** in your code.', + refs: [ + { type: 'group', plugin: 'eslint', slug: 'problems', weight: 1 }, + ], }, - ], - }, -]; - -export const lighthouseCategories: CategoryConfig[] = [ - { - slug: 'performance', - title: 'Performance', - refs: [lighthouseGroupRef('performance')], - }, - { - slug: 'a11y', - title: 'Accessibility', - refs: [lighthouseGroupRef('accessibility')], - }, - { - slug: 'best-practices', - title: 'Best Practices', - refs: [lighthouseGroupRef('best-practices')], - }, - { - slug: 'seo', - title: 'SEO', - refs: [lighthouseGroupRef('seo')], - }, -]; - -export const eslintCategories: CategoryConfig[] = [ - { - slug: 'bug-prevention', - title: 'Bug prevention', - description: 'Lint rules that find **potential bugs** in your code.', - refs: [{ type: 'group', plugin: 'eslint', slug: 'problems', weight: 1 }], - }, - { - slug: 'code-style', - title: 'Code style', - description: - 'Lint rules that promote **good practices** and consistency in your code.', - refs: [{ type: 'group', plugin: 'eslint', slug: 'suggestions', weight: 1 }], - }, -]; - -export function getJsDocsCategories( - config: JsDocsPluginTransformedConfig, -): CategoryConfig[] { - return [ - { - slug: 'docs', - title: 'Documentation', - description: 'Measures how much of your code is **documented**.', - refs: filterGroupsByOnlyAudits(groups, config).map(group => ({ - weight: 1, - type: 'group', - plugin: PLUGIN_SLUG, - slug: group.slug, - })), - }, - ]; -} - -export const coverageCategories: CategoryConfig[] = [ - { - slug: 'code-coverage', - title: 'Code coverage', - description: 'Measures how much of your code is **covered by tests**.', - refs: [ { - type: 'group', - plugin: 'coverage', - slug: 'coverage', - weight: 1, + slug: 'code-style', + title: 'Code style', + description: + 'Lint rules that promote **good practices** and consistency in your code.', + refs: [ + { type: 'group', plugin: 'eslint', slug: 'suggestions', weight: 1 }, + ], }, ], - }, -]; - -export const jsPackagesCoreConfig = async (): Promise => ({ - plugins: [await jsPackagesPlugin()], - categories: jsPackagesCategories, -}); - -export const lighthouseCoreConfig = async ( - urls: PluginUrls, -): Promise => { - const lhPlugin = await lighthousePlugin(urls); - return { - plugins: [lhPlugin], - categories: mergeLighthouseCategories(lhPlugin, lighthouseCategories), }; -}; - -export const jsDocsCoreConfig = ( - config: JsDocsPluginTransformedConfig | string[], -): CoreConfig => ({ - plugins: [ - jsDocsPlugin(Array.isArray(config) ? { patterns: config } : config), - ], - categories: getJsDocsCategories( - Array.isArray(config) ? { patterns: config } : config, - ), -}); - -export async function eslintConfigFromPublishableNxProjects(): Promise< - ESLintTarget[] -> { - const { createProjectGraphAsync } = await import('@nx/devkit'); - const projectGraph = await createProjectGraphAsync({ exitOnError: false }); - return nxProjectsToConfig( - projectGraph, - project => project.tags?.includes('publishable') ?? false, - ); } -export const eslintCoreConfigNx = async ( - projectName?: string, -): Promise => ({ - plugins: [ - projectName - ? await eslintPlugin( - { - eslintrc: `packages/${projectName}/eslint.config.js`, - patterns: ['.'], - }, - { - artifacts: { - // We leverage Nx dependsOn to only run all lint targets before we run code-pushup - // generateArtifactsCommand: 'npx nx run-many -t lint', - artifactsPaths: [ - `packages/${projectName}/.eslint/eslint-report.json`, - ], - }, - }, - ) - : await eslintPlugin(await eslintConfigFromAllNxProjects()), - ], - categories: eslintCategories, -}); - -export const typescriptPluginConfig = async ( - options?: TypescriptPluginOptions, -): Promise => ({ - plugins: [await typescriptPlugin(options)], - categories: getCategories(), -}); - -export const coverageCoreConfigNx = async ( +export async function configureCoveragePlugin( projectName?: string, -): Promise => { +): Promise { const config: CoveragePluginConfig = projectName ? // We do not need to run a coverageToolCommand. This is handled over the Nx task graph. { @@ -223,10 +107,124 @@ export const coverageCoreConfigNx = async ( }; return { plugins: [await coveragePlugin(config)], - categories: coverageCategories, + categories: [ + { + slug: 'code-coverage', + title: 'Code coverage', + description: 'Measures how much of your code is **covered by tests**.', + refs: [ + { type: 'group', plugin: 'coverage', slug: 'coverage', weight: 1 }, + ], + }, + ], + }; +} + +export async function configureJsPackagesPlugin(): Promise { + return { + plugins: [await jsPackagesPlugin()], + categories: [ + { + slug: 'security', + title: 'Security', + description: 'Finds known **vulnerabilities** in 3rd-party packages.', + refs: [ + { + type: 'group', + plugin: 'js-packages', + slug: 'npm-audit', + weight: 1, + }, + ], + }, + { + slug: 'updates', + title: 'Updates', + description: 'Finds **outdated** 3rd-party packages.', + refs: [ + { + type: 'group', + plugin: 'js-packages', + slug: 'npm-outdated', + weight: 1, + }, + ], + }, + ], + }; +} + +export function configureTypescriptPlugin(projectName?: string): CoreConfig { + const tsconfig = projectName + ? `packages/${projectName}/tsconfig.lib.json` + : 'tsconfig.base.json'; + return { + plugins: [typescriptPlugin({ tsconfig })], + categories: getCategories(), + }; +} + +export function configureJsDocsPlugin(projectName?: string): CoreConfig { + const patterns: string[] = [ + `packages/${projectName ?? '*'}/src/**/*.ts`, + `!**/node_modules`, + `!**/{mocks,mock}`, + `!**/*.{spec,test}.ts`, + `!**/implementation/**`, + `!**/internal/**`, + ]; + return { + plugins: [jsDocsPlugin(patterns)], + categories: [ + { + slug: 'docs', + title: 'Documentation', + description: 'Measures how much of your code is **documented**.', + refs: groups.map(group => ({ + weight: 1, + type: 'group', + plugin: PLUGIN_SLUG, + slug: group.slug, + })), + }, + ], + }; +} + +export async function configureLighthousePlugin( + urls: PluginUrls, +): Promise { + const lhPlugin = await lighthousePlugin(urls); + const lhCategories: CategoryConfig[] = [ + { + slug: 'performance', + title: 'Performance', + refs: [lighthouseGroupRef('performance')], + }, + { + slug: 'a11y', + title: 'Accessibility', + refs: [lighthouseGroupRef('accessibility')], + }, + { + slug: 'best-practices', + title: 'Best Practices', + refs: [lighthouseGroupRef('best-practices')], + }, + { + slug: 'seo', + title: 'SEO', + refs: [lighthouseGroupRef('seo')], + }, + ]; + return { + plugins: [lhPlugin], + categories: mergeLighthouseCategories(lhPlugin, lhCategories), }; -}; +} -export const axeCoreConfig = (urls: PluginUrls): CoreConfig => ({ - plugins: [axePlugin(urls)], -}); +export function axeCoreConfig(urls: PluginUrls): CoreConfig { + return { + plugins: [axePlugin(urls)], + }; +} From 4f60265ae6a69ea8f9e1436a13a2b41506404cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Wed, 26 Nov 2025 17:57:36 +0100 Subject: [PATCH 07/16] ci: configure cached code-pushup targets per project --- code-pushup.preset.ts | 19 ++++++------- nx.json | 8 +++--- packages/ci/code-pushup.config.ts | 19 +++++++++++++ packages/ci/project.json | 7 ++++- packages/cli/code-pushup.config.ts | 19 +++++++++++++ packages/cli/project.json | 23 ++++----------- packages/core/code-pushup.config.ts | 19 +++++++++++++ packages/core/project.json | 7 ++++- packages/create-cli/code-pushup.config.ts | 19 +++++++++++++ packages/create-cli/project.json | 15 ++++------ packages/models/code-pushup.config.ts | 28 +++++++++++++++++++ packages/models/project.json | 6 +++- packages/nx-plugin/code-pushup.config.ts | 19 +++++++++++++ packages/nx-plugin/project.json | 7 ++++- packages/plugin-axe/code-pushup.config.ts | 19 +++++++++++++ packages/plugin-axe/project.json | 7 ++++- .../plugin-coverage/code-pushup.config.ts | 19 +++++++++++++ packages/plugin-coverage/project.json | 7 ++++- packages/plugin-eslint/code-pushup.config.ts | 19 +++++++++++++ packages/plugin-eslint/project.json | 7 ++++- .../plugin-js-packages/code-pushup.config.ts | 19 +++++++++++++ packages/plugin-js-packages/project.json | 7 ++++- packages/plugin-jsdocs/code-pushup.config.ts | 19 +++++++++++++ packages/plugin-jsdocs/project.json | 7 ++++- .../plugin-lighthouse/code-pushup.config.ts | 19 +++++++++++++ packages/plugin-lighthouse/project.json | 7 ++++- .../plugin-typescript/code-pushup.config.ts | 19 +++++++++++++ packages/plugin-typescript/project.json | 7 ++++- packages/utils/code-pushup.config.ts | 19 +++++++++++++ packages/utils/project.json | 5 ++++ 30 files changed, 368 insertions(+), 53 deletions(-) create mode 100644 packages/ci/code-pushup.config.ts create mode 100644 packages/cli/code-pushup.config.ts create mode 100644 packages/core/code-pushup.config.ts create mode 100644 packages/create-cli/code-pushup.config.ts create mode 100644 packages/models/code-pushup.config.ts create mode 100644 packages/nx-plugin/code-pushup.config.ts create mode 100644 packages/plugin-axe/code-pushup.config.ts create mode 100644 packages/plugin-coverage/code-pushup.config.ts create mode 100644 packages/plugin-eslint/code-pushup.config.ts create mode 100644 packages/plugin-js-packages/code-pushup.config.ts create mode 100644 packages/plugin-jsdocs/code-pushup.config.ts create mode 100644 packages/plugin-lighthouse/code-pushup.config.ts create mode 100644 packages/plugin-typescript/code-pushup.config.ts create mode 100644 packages/utils/code-pushup.config.ts diff --git a/code-pushup.preset.ts b/code-pushup.preset.ts index 77e2003f0..4323b18e2 100644 --- a/code-pushup.preset.ts +++ b/code-pushup.preset.ts @@ -27,14 +27,14 @@ import typescriptPlugin, { getCategories, } from './packages/plugin-typescript/src/index.js'; -export function configureUpload(): CoreConfig { +export function configureUpload(projectName?: string): CoreConfig { return { ...(process.env['CP_API_KEY'] && { upload: { server: 'https://api.staging.code-pushup.dev/graphql', apiKey: process.env['CP_API_KEY'], organization: 'code-pushup', - project: process.env['NX_TASK_TARGET_PROJECT'] ?? 'cli-workspace', + project: projectName ? `cli-${projectName}` : 'cli-workspace', }, }), plugins: [], @@ -89,20 +89,19 @@ export async function configureEslintPlugin( export async function configureCoveragePlugin( projectName?: string, ): Promise { + const targets = ['unit-test', 'int-test']; const config: CoveragePluginConfig = projectName ? // We do not need to run a coverageToolCommand. This is handled over the Nx task graph. { - reports: [ - { - pathToProject: `packages/${projectName}`, - resultsPath: `packages/${projectName}/coverage/lcov.info`, - }, - ], + reports: targets.map(target => ({ + pathToProject: `packages/${projectName}`, + resultsPath: `coverage/${projectName}/${target}s/lcov.info`, + })), } : { - reports: await getNxCoveragePaths(['unit-test', 'int-test']), + reports: await getNxCoveragePaths(targets), coverageToolCommand: { - command: 'npx nx run-many -t unit-test,int-test', + command: `npx nx run-many -t ${targets.join(',')}`, }, }; return { diff --git a/nx.json b/nx.json index df00ad553..5da90e4b0 100644 --- a/nx.json +++ b/nx.json @@ -12,7 +12,7 @@ "!{projectRoot}/zod2md.config.ts", "!{projectRoot}/eslint.config.?(c)js", "!{workspaceRoot}/**/.code-pushup/**/*", - "!{projectRoot}/code-pushup.config.?(*.).?(m)[jt]s", + "!{projectRoot}/code-pushup.config.?(m)[jt]s", "!{projectRoot}/@(test|mocks|mock)/**/*", "!{projectRoot}/**/?(*.)test.[jt]s?(x)?(.snap)", "!{projectRoot}/**/?(*.)mocks.[jt]s?(x)", @@ -122,8 +122,7 @@ "args": [ "--config={projectRoot}/code-pushup.config.ts", "--cache.read", - "--persist.outputDir={projectRoot}/.code-pushup", - "--upload.project=cli-{projectName}" + "--persist.outputDir={projectRoot}/.code-pushup" ], "env": { "NODE_OPTIONS": "--import tsx", @@ -132,11 +131,11 @@ } }, "code-pushup-coverage": { + "dependsOn": ["*-test"], "cache": true, "inputs": ["default", "code-pushup-inputs"], "outputs": ["{projectRoot}/.code-pushup/coverage/runner-output.json"], "executor": "nx:run-commands", - "dependsOn": ["*-test"], "options": { "command": "node packages/cli/src/index.ts collect", "args": [ @@ -153,6 +152,7 @@ } }, "code-pushup-eslint": { + "dependsOn": ["lint"], "cache": true, "inputs": ["default", "code-pushup-inputs"], "outputs": ["{projectRoot}/.code-pushup/eslint/runner-output.json"], diff --git a/packages/ci/code-pushup.config.ts b/packages/ci/code-pushup.config.ts new file mode 100644 index 000000000..b582535ae --- /dev/null +++ b/packages/ci/code-pushup.config.ts @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import { + configureCoveragePlugin, + configureEslintPlugin, + configureJsDocsPlugin, + configureTypescriptPlugin, + configureUpload, +} from '../../code-pushup.preset.js'; +import { mergeConfigs } from '../utils/src/index.js'; + +const projectName = 'ci'; + +export default mergeConfigs( + configureUpload(projectName), + await configureEslintPlugin(projectName), + await configureCoveragePlugin(projectName), + configureTypescriptPlugin(projectName), + configureJsDocsPlugin(projectName), +); diff --git a/packages/ci/project.json b/packages/ci/project.json index b6bdeab2c..b17701454 100644 --- a/packages/ci/project.json +++ b/packages/ci/project.json @@ -8,7 +8,12 @@ "lint": {}, "lint-report": {}, "unit-test": {}, - "int-test": {} + "int-test": {}, + "code-pushup": {}, + "code-pushup-eslint": {}, + "code-pushup-coverage": {}, + "code-pushup-typescript": {}, + "code-pushup-jsdocs": {} }, "tags": ["scope:tooling", "type:feature", "publishable"] } diff --git a/packages/cli/code-pushup.config.ts b/packages/cli/code-pushup.config.ts new file mode 100644 index 000000000..0fb99f57a --- /dev/null +++ b/packages/cli/code-pushup.config.ts @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import { + configureCoveragePlugin, + configureEslintPlugin, + configureJsDocsPlugin, + configureTypescriptPlugin, + configureUpload, +} from '../../code-pushup.preset.js'; +import { mergeConfigs } from '../utils/src/index.js'; + +const projectName = 'cli'; + +export default mergeConfigs( + configureUpload(projectName), + await configureEslintPlugin(projectName), + await configureCoveragePlugin(projectName), + configureTypescriptPlugin(projectName), + configureJsDocsPlugin(projectName), +); diff --git a/packages/cli/project.json b/packages/cli/project.json index 733186813..99d492c1c 100644 --- a/packages/cli/project.json +++ b/packages/cli/project.json @@ -9,24 +9,11 @@ "lint-report": {}, "unit-test": {}, "int-test": {}, - "run-help": { - "command": "npx dist/packages/cli --help", - "dependsOn": ["build"] - }, - "run-collect": { - "command": "npx ../../dist/packages/cli collect --persist.format=json --persist.format=md", - "options": { - "cwd": "examples/react-todos-app" - }, - "dependsOn": ["build"] - }, - "run-print-config": { - "command": "npx ../../dist/packages/cli print-config", - "options": { - "cwd": "examples/react-todos-app" - }, - "dependsOn": ["build"] - } + "code-pushup": {}, + "code-pushup-eslint": {}, + "code-pushup-coverage": {}, + "code-pushup-typescript": {}, + "code-pushup-jsdocs": {} }, "tags": ["scope:core", "type:app", "publishable"] } diff --git a/packages/core/code-pushup.config.ts b/packages/core/code-pushup.config.ts new file mode 100644 index 000000000..4da9f7c81 --- /dev/null +++ b/packages/core/code-pushup.config.ts @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import { + configureCoveragePlugin, + configureEslintPlugin, + configureJsDocsPlugin, + configureTypescriptPlugin, + configureUpload, +} from '../../code-pushup.preset.js'; +import { mergeConfigs } from '../utils/src/index.js'; + +const projectName = 'core'; + +export default mergeConfigs( + configureUpload(projectName), + await configureEslintPlugin(projectName), + await configureCoveragePlugin(projectName), + configureTypescriptPlugin(projectName), + configureJsDocsPlugin(projectName), +); diff --git a/packages/core/project.json b/packages/core/project.json index 75fc9f380..6885dd7e2 100644 --- a/packages/core/project.json +++ b/packages/core/project.json @@ -8,7 +8,12 @@ "lint": {}, "lint-report": {}, "unit-test": {}, - "int-test": {} + "int-test": {}, + "code-pushup": {}, + "code-pushup-eslint": {}, + "code-pushup-coverage": {}, + "code-pushup-typescript": {}, + "code-pushup-jsdocs": {} }, "tags": ["scope:core", "type:feature", "publishable"] } diff --git a/packages/create-cli/code-pushup.config.ts b/packages/create-cli/code-pushup.config.ts new file mode 100644 index 000000000..5db62c96f --- /dev/null +++ b/packages/create-cli/code-pushup.config.ts @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import { + configureCoveragePlugin, + configureEslintPlugin, + configureJsDocsPlugin, + configureTypescriptPlugin, + configureUpload, +} from '../../code-pushup.preset.js'; +import { mergeConfigs } from '../utils/src/index.js'; + +const projectName = 'create-cli'; + +export default mergeConfigs( + configureUpload(projectName), + await configureEslintPlugin(projectName), + await configureCoveragePlugin(projectName), + configureTypescriptPlugin(projectName), + configureJsDocsPlugin(projectName), +); diff --git a/packages/create-cli/project.json b/packages/create-cli/project.json index 813f42b91..74ca87634 100644 --- a/packages/create-cli/project.json +++ b/packages/create-cli/project.json @@ -8,16 +8,11 @@ "lint": {}, "lint-report": {}, "unit-test": {}, - "exec-node": { - "dependsOn": ["build"], - "command": "node ./dist/packages/create-cli/src/index.js", - "options": {} - }, - "exec-npm": { - "dependsOn": ["^build"], - "command": "npm exec ./dist/packages/create-cli", - "options": {} - } + "code-pushup": {}, + "code-pushup-eslint": {}, + "code-pushup-coverage": {}, + "code-pushup-typescript": {}, + "code-pushup-jsdocs": {} }, "tags": ["scope:tooling", "type:app", "publishable"] } diff --git a/packages/models/code-pushup.config.ts b/packages/models/code-pushup.config.ts new file mode 100644 index 000000000..f34ecdcde --- /dev/null +++ b/packages/models/code-pushup.config.ts @@ -0,0 +1,28 @@ +import 'dotenv/config'; +import { + configureCoveragePlugin, + configureEslintPlugin, + configureJsDocsPlugin, + configureUpload, +} from '../../code-pushup.preset.js'; +import type { CoreConfig } from './src/index.js'; + +const projectName = 'models'; + +// cannot use mergeConfigs from utils package, would create cycle in Nx graph +const config: CoreConfig = [ + await configureEslintPlugin(projectName), + await configureCoveragePlugin(projectName), + // FIXME: Can't create TS program in getDiagnostics. Cannot find module './packages/models/transformers/dist' + // configureTypescriptPlugin(projectName), + configureJsDocsPlugin(projectName), +].reduce( + (acc, { plugins, categories }) => ({ + ...acc, + plugins: [...acc.plugins, ...plugins], + categories: [...(acc.categories ?? []), ...(categories ?? [])], + }), + configureUpload(projectName), +); + +export default config; diff --git a/packages/models/project.json b/packages/models/project.json index 70591234a..aaae33e1d 100644 --- a/packages/models/project.json +++ b/packages/models/project.json @@ -26,7 +26,11 @@ }, "lint": {}, "lint-report": {}, - "unit-test": {} + "unit-test": {}, + "code-pushup": {}, + "code-pushup-eslint": {}, + "code-pushup-coverage": {}, + "code-pushup-jsdocs": {} }, "tags": ["scope:shared", "type:util", "publishable"] } diff --git a/packages/nx-plugin/code-pushup.config.ts b/packages/nx-plugin/code-pushup.config.ts new file mode 100644 index 000000000..98c56397a --- /dev/null +++ b/packages/nx-plugin/code-pushup.config.ts @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import { + configureCoveragePlugin, + configureEslintPlugin, + configureJsDocsPlugin, + configureTypescriptPlugin, + configureUpload, +} from '../../code-pushup.preset.js'; +import { mergeConfigs } from '../utils/src/index.js'; + +const projectName = 'nx-plugin'; + +export default mergeConfigs( + configureUpload(projectName), + await configureEslintPlugin(projectName), + await configureCoveragePlugin(projectName), + configureTypescriptPlugin(projectName), + configureJsDocsPlugin(projectName), +); diff --git a/packages/nx-plugin/project.json b/packages/nx-plugin/project.json index 63e15c1d4..247770900 100644 --- a/packages/nx-plugin/project.json +++ b/packages/nx-plugin/project.json @@ -51,7 +51,12 @@ } }, "unit-test": {}, - "int-test": {} + "int-test": {}, + "code-pushup": {}, + "code-pushup-eslint": {}, + "code-pushup-coverage": {}, + "code-pushup-typescript": {}, + "code-pushup-jsdocs": {} }, "tags": ["scope:tooling", "type:feature", "publishable"] } diff --git a/packages/plugin-axe/code-pushup.config.ts b/packages/plugin-axe/code-pushup.config.ts new file mode 100644 index 000000000..f13bb57e5 --- /dev/null +++ b/packages/plugin-axe/code-pushup.config.ts @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import { + configureCoveragePlugin, + configureEslintPlugin, + configureJsDocsPlugin, + configureTypescriptPlugin, + configureUpload, +} from '../../code-pushup.preset.js'; +import { mergeConfigs } from '../utils/src/index.js'; + +const projectName = 'plugin-axe'; + +export default mergeConfigs( + configureUpload(projectName), + await configureEslintPlugin(projectName), + await configureCoveragePlugin(projectName), + configureTypescriptPlugin(projectName), + configureJsDocsPlugin(projectName), +); diff --git a/packages/plugin-axe/project.json b/packages/plugin-axe/project.json index 34238ce83..86cba9d5c 100644 --- a/packages/plugin-axe/project.json +++ b/packages/plugin-axe/project.json @@ -8,6 +8,11 @@ "targets": { "build": {}, "lint": {}, - "unit-test": {} + "unit-test": {}, + "code-pushup": {}, + "code-pushup-eslint": {}, + "code-pushup-coverage": {}, + "code-pushup-typescript": {}, + "code-pushup-jsdocs": {} } } diff --git a/packages/plugin-coverage/code-pushup.config.ts b/packages/plugin-coverage/code-pushup.config.ts new file mode 100644 index 000000000..ff0c6710d --- /dev/null +++ b/packages/plugin-coverage/code-pushup.config.ts @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import { + configureCoveragePlugin, + configureEslintPlugin, + configureJsDocsPlugin, + configureTypescriptPlugin, + configureUpload, +} from '../../code-pushup.preset.js'; +import { mergeConfigs } from '../utils/src/index.js'; + +const projectName = 'plugin-coverage'; + +export default mergeConfigs( + configureUpload(projectName), + await configureEslintPlugin(projectName), + await configureCoveragePlugin(projectName), + configureTypescriptPlugin(projectName), + configureJsDocsPlugin(projectName), +); diff --git a/packages/plugin-coverage/project.json b/packages/plugin-coverage/project.json index 031d65697..0dd270a15 100644 --- a/packages/plugin-coverage/project.json +++ b/packages/plugin-coverage/project.json @@ -8,7 +8,12 @@ "lint": {}, "lint-report": {}, "unit-test": {}, - "int-test": {} + "int-test": {}, + "code-pushup": {}, + "code-pushup-eslint": {}, + "code-pushup-coverage": {}, + "code-pushup-typescript": {}, + "code-pushup-jsdocs": {} }, "tags": ["scope:plugin", "type:feature", "publishable"] } diff --git a/packages/plugin-eslint/code-pushup.config.ts b/packages/plugin-eslint/code-pushup.config.ts new file mode 100644 index 000000000..7e96d9ba6 --- /dev/null +++ b/packages/plugin-eslint/code-pushup.config.ts @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import { + configureCoveragePlugin, + configureEslintPlugin, + configureJsDocsPlugin, + configureTypescriptPlugin, + configureUpload, +} from '../../code-pushup.preset.js'; +import { mergeConfigs } from '../utils/src/index.js'; + +const projectName = 'plugin-eslint'; + +export default mergeConfigs( + configureUpload(projectName), + await configureEslintPlugin(projectName), + await configureCoveragePlugin(projectName), + configureTypescriptPlugin(projectName), + configureJsDocsPlugin(projectName), +); diff --git a/packages/plugin-eslint/project.json b/packages/plugin-eslint/project.json index eeae19055..323c3ce9a 100644 --- a/packages/plugin-eslint/project.json +++ b/packages/plugin-eslint/project.json @@ -8,7 +8,12 @@ "lint": {}, "lint-report": {}, "unit-test": {}, - "int-test": {} + "int-test": {}, + "code-pushup": {}, + "code-pushup-eslint": {}, + "code-pushup-coverage": {}, + "code-pushup-typescript": {}, + "code-pushup-jsdocs": {} }, "tags": ["scope:plugin", "type:feature", "publishable"] } diff --git a/packages/plugin-js-packages/code-pushup.config.ts b/packages/plugin-js-packages/code-pushup.config.ts new file mode 100644 index 000000000..bfdb9da88 --- /dev/null +++ b/packages/plugin-js-packages/code-pushup.config.ts @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import { + configureCoveragePlugin, + configureEslintPlugin, + configureJsDocsPlugin, + configureTypescriptPlugin, + configureUpload, +} from '../../code-pushup.preset.js'; +import { mergeConfigs } from '../utils/src/index.js'; + +const projectName = 'plugin-js-packages'; + +export default mergeConfigs( + configureUpload(projectName), + await configureEslintPlugin(projectName), + await configureCoveragePlugin(projectName), + configureTypescriptPlugin(projectName), + configureJsDocsPlugin(projectName), +); diff --git a/packages/plugin-js-packages/project.json b/packages/plugin-js-packages/project.json index 63782308a..1cf5c7677 100644 --- a/packages/plugin-js-packages/project.json +++ b/packages/plugin-js-packages/project.json @@ -8,7 +8,12 @@ "lint": {}, "lint-report": {}, "unit-test": {}, - "int-test": {} + "int-test": {}, + "code-pushup": {}, + "code-pushup-eslint": {}, + "code-pushup-coverage": {}, + "code-pushup-typescript": {}, + "code-pushup-jsdocs": {} }, "tags": ["scope:plugin", "type:feature", "publishable"], "description": "A plugin for JavaScript packages." diff --git a/packages/plugin-jsdocs/code-pushup.config.ts b/packages/plugin-jsdocs/code-pushup.config.ts new file mode 100644 index 000000000..17188ebbe --- /dev/null +++ b/packages/plugin-jsdocs/code-pushup.config.ts @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import { + configureCoveragePlugin, + configureEslintPlugin, + configureJsDocsPlugin, + configureTypescriptPlugin, + configureUpload, +} from '../../code-pushup.preset.js'; +import { mergeConfigs } from '../utils/src/index.js'; + +const projectName = 'plugin-jsdocs'; + +export default mergeConfigs( + configureUpload(projectName), + await configureEslintPlugin(projectName), + await configureCoveragePlugin(projectName), + configureTypescriptPlugin(projectName), + configureJsDocsPlugin(projectName), +); diff --git a/packages/plugin-jsdocs/project.json b/packages/plugin-jsdocs/project.json index 9f07a4761..a721b2895 100644 --- a/packages/plugin-jsdocs/project.json +++ b/packages/plugin-jsdocs/project.json @@ -9,6 +9,11 @@ "lint": {}, "lint-report": {}, "unit-test": {}, - "int-test": {} + "int-test": {}, + "code-pushup": {}, + "code-pushup-eslint": {}, + "code-pushup-coverage": {}, + "code-pushup-typescript": {}, + "code-pushup-jsdocs": {} } } diff --git a/packages/plugin-lighthouse/code-pushup.config.ts b/packages/plugin-lighthouse/code-pushup.config.ts new file mode 100644 index 000000000..6debb767d --- /dev/null +++ b/packages/plugin-lighthouse/code-pushup.config.ts @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import { + configureCoveragePlugin, + configureEslintPlugin, + configureJsDocsPlugin, + configureTypescriptPlugin, + configureUpload, +} from '../../code-pushup.preset.js'; +import { mergeConfigs } from '../utils/src/index.js'; + +const projectName = 'plugin-lighthouse'; + +export default mergeConfigs( + configureUpload(projectName), + await configureEslintPlugin(projectName), + await configureCoveragePlugin(projectName), + configureTypescriptPlugin(projectName), + configureJsDocsPlugin(projectName), +); diff --git a/packages/plugin-lighthouse/project.json b/packages/plugin-lighthouse/project.json index 468165a9c..68c4ed888 100644 --- a/packages/plugin-lighthouse/project.json +++ b/packages/plugin-lighthouse/project.json @@ -7,7 +7,12 @@ "build": {}, "lint": {}, "lint-report": {}, - "unit-test": {} + "unit-test": {}, + "code-pushup": {}, + "code-pushup-eslint": {}, + "code-pushup-coverage": {}, + "code-pushup-typescript": {}, + "code-pushup-jsdocs": {} }, "tags": ["scope:plugin", "type:feature", "publishable"] } diff --git a/packages/plugin-typescript/code-pushup.config.ts b/packages/plugin-typescript/code-pushup.config.ts new file mode 100644 index 000000000..5a2915448 --- /dev/null +++ b/packages/plugin-typescript/code-pushup.config.ts @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import { + configureCoveragePlugin, + configureEslintPlugin, + configureJsDocsPlugin, + configureTypescriptPlugin, + configureUpload, +} from '../../code-pushup.preset.js'; +import { mergeConfigs } from '../utils/src/index.js'; + +const projectName = 'plugin-typescript'; + +export default mergeConfigs( + configureUpload(projectName), + await configureEslintPlugin(projectName), + await configureCoveragePlugin(projectName), + configureTypescriptPlugin(projectName), + configureJsDocsPlugin(projectName), +); diff --git a/packages/plugin-typescript/project.json b/packages/plugin-typescript/project.json index de10ea00e..804cd22be 100644 --- a/packages/plugin-typescript/project.json +++ b/packages/plugin-typescript/project.json @@ -8,7 +8,12 @@ "lint": {}, "lint-report": {}, "unit-test": {}, - "int-test": {} + "int-test": {}, + "code-pushup": {}, + "code-pushup-eslint": {}, + "code-pushup-coverage": {}, + "code-pushup-typescript": {}, + "code-pushup-jsdocs": {} }, "tags": ["scope:plugin", "type:feature", "publishable"] } diff --git a/packages/utils/code-pushup.config.ts b/packages/utils/code-pushup.config.ts new file mode 100644 index 000000000..c47945a37 --- /dev/null +++ b/packages/utils/code-pushup.config.ts @@ -0,0 +1,19 @@ +import 'dotenv/config'; +import { + configureCoveragePlugin, + configureEslintPlugin, + configureJsDocsPlugin, + configureTypescriptPlugin, + configureUpload, +} from '../../code-pushup.preset.js'; +import { mergeConfigs } from '../utils/src/index.js'; + +const projectName = 'utils'; + +export default mergeConfigs( + configureUpload(projectName), + await configureEslintPlugin(projectName), + await configureCoveragePlugin(projectName), + configureTypescriptPlugin(projectName), + configureJsDocsPlugin(projectName), +); diff --git a/packages/utils/project.json b/packages/utils/project.json index 0ddab978c..4e0e5c4ac 100644 --- a/packages/utils/project.json +++ b/packages/utils/project.json @@ -21,6 +21,11 @@ }, "unit-test": {}, "int-test": {}, + "code-pushup": {}, + "code-pushup-eslint": {}, + "code-pushup-coverage": {}, + "code-pushup-typescript": {}, + "code-pushup-jsdocs": {}, "demo-logger": { "executor": "nx:run-commands", "options": { From 4444f3bbecc934e5fa609a0aed3e712423ffc5de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Wed, 26 Nov 2025 17:59:39 +0100 Subject: [PATCH 08/16] ci: run code-pushup on self in monorepo mode --- .github/actions/code-pushup/src/runner.ts | 2 +- .github/workflows/code-pushup.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/code-pushup/src/runner.ts b/.github/actions/code-pushup/src/runner.ts index 47c5d3dd2..82c9bd7df 100644 --- a/.github/actions/code-pushup/src/runner.ts +++ b/.github/actions/code-pushup/src/runner.ts @@ -135,7 +135,7 @@ async function run(): Promise { } const options: Options = { - bin: 'npx nx code-pushup --nx-bail --', + monorepo: true, }; const gitRefs = parseGitRefs(); diff --git a/.github/workflows/code-pushup.yml b/.github/workflows/code-pushup.yml index 9cf5477ca..7ed679c14 100644 --- a/.github/workflows/code-pushup.yml +++ b/.github/workflows/code-pushup.yml @@ -1,4 +1,4 @@ -name: Code PushUp - Standalone Mode +name: Code PushUp on: push: From 9aad4183a0fbaa65869fc036adeefc4a69f597fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Wed, 26 Nov 2025 18:02:27 +0100 Subject: [PATCH 09/16] chore: remove incomplete lint-report targets --- packages/ci/project.json | 1 - packages/cli/project.json | 1 - packages/core/project.json | 1 - packages/create-cli/project.json | 2 +- packages/models/project.json | 1 - packages/plugin-coverage/project.json | 1 - packages/plugin-eslint/project.json | 1 - packages/plugin-js-packages/project.json | 1 - packages/plugin-jsdocs/project.json | 1 - packages/plugin-lighthouse/project.json | 1 - packages/plugin-typescript/project.json | 1 - packages/utils/project.json | 1 - 12 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/ci/project.json b/packages/ci/project.json index b17701454..41fd8cda7 100644 --- a/packages/ci/project.json +++ b/packages/ci/project.json @@ -6,7 +6,6 @@ "targets": { "build": {}, "lint": {}, - "lint-report": {}, "unit-test": {}, "int-test": {}, "code-pushup": {}, diff --git a/packages/cli/project.json b/packages/cli/project.json index 99d492c1c..c92b12115 100644 --- a/packages/cli/project.json +++ b/packages/cli/project.json @@ -6,7 +6,6 @@ "targets": { "build": {}, "lint": {}, - "lint-report": {}, "unit-test": {}, "int-test": {}, "code-pushup": {}, diff --git a/packages/core/project.json b/packages/core/project.json index 6885dd7e2..6ffc72788 100644 --- a/packages/core/project.json +++ b/packages/core/project.json @@ -6,7 +6,6 @@ "targets": { "build": {}, "lint": {}, - "lint-report": {}, "unit-test": {}, "int-test": {}, "code-pushup": {}, diff --git a/packages/create-cli/project.json b/packages/create-cli/project.json index 74ca87634..8e4af8b5b 100644 --- a/packages/create-cli/project.json +++ b/packages/create-cli/project.json @@ -6,7 +6,7 @@ "targets": { "build": {}, "lint": {}, - "lint-report": {}, + "unit-test": {}, "code-pushup": {}, "code-pushup-eslint": {}, diff --git a/packages/models/project.json b/packages/models/project.json index aaae33e1d..0045b607e 100644 --- a/packages/models/project.json +++ b/packages/models/project.json @@ -25,7 +25,6 @@ ] }, "lint": {}, - "lint-report": {}, "unit-test": {}, "code-pushup": {}, "code-pushup-eslint": {}, diff --git a/packages/plugin-coverage/project.json b/packages/plugin-coverage/project.json index 0dd270a15..04a76ed34 100644 --- a/packages/plugin-coverage/project.json +++ b/packages/plugin-coverage/project.json @@ -6,7 +6,6 @@ "targets": { "build": {}, "lint": {}, - "lint-report": {}, "unit-test": {}, "int-test": {}, "code-pushup": {}, diff --git a/packages/plugin-eslint/project.json b/packages/plugin-eslint/project.json index 323c3ce9a..beabbc297 100644 --- a/packages/plugin-eslint/project.json +++ b/packages/plugin-eslint/project.json @@ -6,7 +6,6 @@ "targets": { "build": {}, "lint": {}, - "lint-report": {}, "unit-test": {}, "int-test": {}, "code-pushup": {}, diff --git a/packages/plugin-js-packages/project.json b/packages/plugin-js-packages/project.json index 1cf5c7677..5ef4c7cf1 100644 --- a/packages/plugin-js-packages/project.json +++ b/packages/plugin-js-packages/project.json @@ -6,7 +6,6 @@ "targets": { "build": {}, "lint": {}, - "lint-report": {}, "unit-test": {}, "int-test": {}, "code-pushup": {}, diff --git a/packages/plugin-jsdocs/project.json b/packages/plugin-jsdocs/project.json index a721b2895..4fb8fb531 100644 --- a/packages/plugin-jsdocs/project.json +++ b/packages/plugin-jsdocs/project.json @@ -7,7 +7,6 @@ "targets": { "build": {}, "lint": {}, - "lint-report": {}, "unit-test": {}, "int-test": {}, "code-pushup": {}, diff --git a/packages/plugin-lighthouse/project.json b/packages/plugin-lighthouse/project.json index 68c4ed888..5ce9c8643 100644 --- a/packages/plugin-lighthouse/project.json +++ b/packages/plugin-lighthouse/project.json @@ -6,7 +6,6 @@ "targets": { "build": {}, "lint": {}, - "lint-report": {}, "unit-test": {}, "code-pushup": {}, "code-pushup-eslint": {}, diff --git a/packages/plugin-typescript/project.json b/packages/plugin-typescript/project.json index 804cd22be..f6da89dc2 100644 --- a/packages/plugin-typescript/project.json +++ b/packages/plugin-typescript/project.json @@ -6,7 +6,6 @@ "targets": { "build": {}, "lint": {}, - "lint-report": {}, "unit-test": {}, "int-test": {}, "code-pushup": {}, diff --git a/packages/utils/project.json b/packages/utils/project.json index 4e0e5c4ac..c8d085a88 100644 --- a/packages/utils/project.json +++ b/packages/utils/project.json @@ -6,7 +6,6 @@ "targets": { "build": {}, "lint": {}, - "lint-report": {}, "perf": { "command": "npx tsx --tsconfig=../tsconfig.perf.json", "options": { From 01ef84a93a95ade576c67d46bfa9221b9a46a9bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Thu, 27 Nov 2025 16:24:32 +0100 Subject: [PATCH 10/16] ci: fix coverage plugin for projects with missing int-test target --- code-pushup.preset.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/code-pushup.preset.ts b/code-pushup.preset.ts index 4323b18e2..5d56acaa2 100644 --- a/code-pushup.preset.ts +++ b/code-pushup.preset.ts @@ -1,4 +1,5 @@ /* eslint-disable @nx/enforce-module-boundaries */ +import { createProjectGraphAsync } from '@nx/devkit'; import type { CategoryConfig, CoreConfig, @@ -93,10 +94,15 @@ export async function configureCoveragePlugin( const config: CoveragePluginConfig = projectName ? // We do not need to run a coverageToolCommand. This is handled over the Nx task graph. { - reports: targets.map(target => ({ - pathToProject: `packages/${projectName}`, - resultsPath: `coverage/${projectName}/${target}s/lcov.info`, - })), + reports: Object.keys( + (await createProjectGraphAsync()).nodes[projectName]?.data.targets ?? + {}, + ) + .filter(target => targets.includes(target)) + .map(target => ({ + pathToProject: `packages/${projectName}`, + resultsPath: `coverage/${projectName}/${target}s/lcov.info`, + })), } : { reports: await getNxCoveragePaths(targets), From 5fd959952591f2175ecdaccfb89a4bdaab95cb84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Thu, 27 Nov 2025 16:38:02 +0100 Subject: [PATCH 11/16] ci: move code-pushup output dir to workspace root --- nx.json | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/nx.json b/nx.json index 5da90e4b0..e0a4aa68c 100644 --- a/nx.json +++ b/nx.json @@ -122,7 +122,7 @@ "args": [ "--config={projectRoot}/code-pushup.config.ts", "--cache.read", - "--persist.outputDir={projectRoot}/.code-pushup" + "--persist.outputDir=.code-pushup/{projectName}" ], "env": { "NODE_OPTIONS": "--import tsx", @@ -134,7 +134,9 @@ "dependsOn": ["*-test"], "cache": true, "inputs": ["default", "code-pushup-inputs"], - "outputs": ["{projectRoot}/.code-pushup/coverage/runner-output.json"], + "outputs": [ + "{workspaceRoot}/.code-pushup/{projectName}/coverage/runner-output.json" + ], "executor": "nx:run-commands", "options": { "command": "node packages/cli/src/index.ts collect", @@ -143,7 +145,7 @@ "--cache.write", "--onlyPlugins=coverage", "--persist.skipReports=true", - "--persist.outputDir={projectRoot}/.code-pushup" + "--persist.outputDir=.code-pushup/{projectName}" ], "env": { "NODE_OPTIONS": "--import tsx", @@ -155,7 +157,9 @@ "dependsOn": ["lint"], "cache": true, "inputs": ["default", "code-pushup-inputs"], - "outputs": ["{projectRoot}/.code-pushup/eslint/runner-output.json"], + "outputs": [ + "{workspaceRoot}/.code-pushup/{projectName}/eslint/runner-output.json" + ], "executor": "nx:run-commands", "options": { "command": "node packages/cli/src/index.ts collect", @@ -164,7 +168,7 @@ "--cache.write", "--onlyPlugins=eslint", "--persist.skipReports", - "--persist.outputDir={projectRoot}/.code-pushup" + "--persist.outputDir=.code-pushup/{projectName}" ], "env": { "NODE_OPTIONS": "--import tsx", @@ -179,14 +183,16 @@ "runtime": "date +%Y-%m-%d" } ], - "outputs": ["{projectRoot}/.code-pushup/js-packages/runner-output.json"], + "outputs": [ + "{workspaceRoot}/.code-pushup/{projectName}/js-packages/runner-output.json" + ], "executor": "nx:run-commands", "options": { "command": "node packages/cli/src/index.ts collect", "args": [ "--config={projectRoot}/code-pushup.config.ts", "--onlyPlugins=js-packages", - "--persist.outputDir={projectRoot}/.code-pushup" + "--persist.outputDir=.code-pushup/{projectName}" ], "env": { "NODE_OPTIONS": "--import tsx", @@ -197,7 +203,9 @@ "code-pushup-lighthouse": { "cache": true, "inputs": ["production", "^production", "code-pushup-inputs"], - "outputs": ["{projectRoot}/.code-pushup/lighthouse/runner-output.json"], + "outputs": [ + "{workspaceRoot}/.code-pushup/{projectName}/lighthouse/runner-output.json" + ], "executor": "nx:run-commands", "options": { "command": "node packages/cli/src/index.ts collect", @@ -206,7 +214,7 @@ "--cache.write", "--onlyPlugins=lighthouse", "--persist.skipReports", - "--persist.outputDir={projectRoot}/.code-pushup" + "--persist.outputDir=.code-pushup/{projectName}" ], "env": { "NODE_OPTIONS": "--import tsx", @@ -221,7 +229,9 @@ "code-pushup-inputs", "typecheck-typescript-inputs" ], - "outputs": ["{projectRoot}/.code-pushup/jsdocs/runner-output.json"], + "outputs": [ + "{workspaceRoot}/.code-pushup/{projectName}/jsdocs/runner-output.json" + ], "executor": "nx:run-commands", "options": { "command": "node packages/cli/src/index.ts collect", @@ -230,7 +240,7 @@ "--cache.write", "--onlyPlugins=jsdocs", "--persist.skipReports", - "--persist.outputDir={projectRoot}/.code-pushup" + "--persist.outputDir=.code-pushup/{projectName}" ], "env": { "NODE_OPTIONS": "--import tsx", @@ -245,7 +255,9 @@ "code-pushup-inputs", "typecheck-typescript-inputs" ], - "outputs": ["{projectRoot}/.code-pushup/typescript/runner-output.json"], + "outputs": [ + "{workspaceRoot}/.code-pushup/{projectName}/typescript/runner-output.json" + ], "executor": "nx:run-commands", "options": { "command": "node packages/cli/src/index.ts collect", @@ -254,7 +266,7 @@ "--cache.write", "--onlyPlugins=typescript", "--persist.skipReports", - "--persist.outputDir={projectRoot}/.code-pushup" + "--persist.outputDir=.code-pushup/{projectName}" ], "env": { "NODE_OPTIONS": "--import tsx", @@ -265,7 +277,9 @@ "code-pushup-axe": { "cache": true, "inputs": ["default", "code-pushup-inputs"], - "outputs": ["{projectRoot}/.code-pushup/axe/runner-output.json"], + "outputs": [ + "{workspaceRoot}/.code-pushup/{projectName}/axe/runner-output.json" + ], "executor": "nx:run-commands", "options": { "command": "node packages/cli/src/index.ts collect", @@ -273,7 +287,7 @@ "--config={projectRoot}/code-pushup.config.ts", "--cache.write", "--onlyPlugins=axe", - "--persist.outputDir={projectRoot}/.code-pushup" + "--persist.outputDir=.code-pushup/{projectName}" ], "env": { "NODE_OPTIONS": "--import tsx", From 2429f4a0db0c05e52980dd5a23c674a95738a5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Thu, 27 Nov 2025 16:42:17 +0100 Subject: [PATCH 12/16] ci(local-action): skip print-config commands by defining configPatterns --- .github/actions/code-pushup/src/runner.ts | 15 +++++++++++++++ code-pushup.preset.ts | 4 ++-- project.json | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/actions/code-pushup/src/runner.ts b/.github/actions/code-pushup/src/runner.ts index 82c9bd7df..95463c2ea 100644 --- a/.github/actions/code-pushup/src/runner.ts +++ b/.github/actions/code-pushup/src/runner.ts @@ -10,6 +10,7 @@ import { type SourceFileIssue, runInCI, } from '@code-pushup/ci'; +import { DEFAULT_PERSIST_CONFIG } from '@code-pushup/models'; import { CODE_PUSHUP_UNICODE_LOGO, logger, @@ -136,6 +137,20 @@ async function run(): Promise { const options: Options = { monorepo: true, + configPatterns: { + persist: { + ...DEFAULT_PERSIST_CONFIG, + outputDir: '.code-pushup/{projectName}', + }, + ...(process.env['CP_API_KEY'] && { + upload: { + server: 'https://api.staging.code-pushup.dev/graphql', + apiKey: process.env['CP_API_KEY'], + organization: 'code-pushup', + project: 'cli-{projectName}', + }, + }), + }, }; const gitRefs = parseGitRefs(); diff --git a/code-pushup.preset.ts b/code-pushup.preset.ts index 5d56acaa2..99660378b 100644 --- a/code-pushup.preset.ts +++ b/code-pushup.preset.ts @@ -28,14 +28,14 @@ import typescriptPlugin, { getCategories, } from './packages/plugin-typescript/src/index.js'; -export function configureUpload(projectName?: string): CoreConfig { +export function configureUpload(projectName: string = 'workspace'): CoreConfig { return { ...(process.env['CP_API_KEY'] && { upload: { server: 'https://api.staging.code-pushup.dev/graphql', apiKey: process.env['CP_API_KEY'], organization: 'code-pushup', - project: projectName ? `cli-${projectName}` : 'cli-workspace', + project: `cli-${projectName}`, }, }), plugins: [], diff --git a/project.json b/project.json index bde78f599..48e03fd94 100644 --- a/project.json +++ b/project.json @@ -1,5 +1,5 @@ { - "name": "cli-workspace", + "name": "workspace", "$schema": "node_modules/nx/schemas/project-schema.json", "targets": { "code-pushup": { From 058e4bd916043b769d8a72e55b92b54394477bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Fri, 28 Nov 2025 14:28:45 +0100 Subject: [PATCH 13/16] ci: fix circular lint errors from bundled configs --- .gitignore | 2 ++ nx.json | 1 + 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 5e706fa10..31d7e1678 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,5 @@ vite.config.*.timestamp* vitest.config.*.timestamp* .cursor/rules/nx-rules.mdc .github/instructions/nx.instructions.md + +code-pushup.config.bundled_*.mjs \ No newline at end of file diff --git a/nx.json b/nx.json index e0a4aa68c..648165e0f 100644 --- a/nx.json +++ b/nx.json @@ -13,6 +13,7 @@ "!{projectRoot}/eslint.config.?(c)js", "!{workspaceRoot}/**/.code-pushup/**/*", "!{projectRoot}/code-pushup.config.?(m)[jt]s", + "!{projectRoot}/code-pushup.config.bundled_*.mjs", "!{projectRoot}/@(test|mocks|mock)/**/*", "!{projectRoot}/**/?(*.)test.[jt]s?(x)?(.snap)", "!{projectRoot}/**/?(*.)mocks.[jt]s?(x)", From 138fe3de52abe59fb083a471b53a8e5273cb7240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Fri, 28 Nov 2025 15:55:04 +0100 Subject: [PATCH 14/16] feat(ci): add jobId option to prevent conflicting PR comments --- packages/ci/README.md | 31 +++++++++-------- packages/ci/src/lib/cli/context.unit.test.ts | 21 ++---------- packages/ci/src/lib/comment.ts | 7 ++-- packages/ci/src/lib/comment.unit.test.ts | 36 +++++++++++++++++--- packages/ci/src/lib/models.ts | 1 + packages/ci/src/lib/run-monorepo.ts | 2 +- packages/ci/src/lib/run-standalone.ts | 2 +- packages/ci/src/lib/settings.ts | 1 + 8 files changed, 59 insertions(+), 42 deletions(-) diff --git a/packages/ci/README.md b/packages/ci/README.md index 23d277d05..a2e91dd2d 100644 --- a/packages/ci/README.md +++ b/packages/ci/README.md @@ -94,21 +94,22 @@ A `Comment` object has the following required properties: Optionally, you can override default options for further customization: -| Property | Type | Default | Description | -| :----------------- | :------------------------ | :------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `monorepo` | `boolean \| MonorepoTool` | `false` | Enables [monorepo mode](#monorepo-mode) | -| `parallel` | `boolean \| number` | `false` | Enables parallel execution in [monorepo mode](#monorepo-mode) | -| `projects` | `string[] \| null` | `null` | Custom projects configuration for [monorepo mode](#monorepo-mode) | -| `task` | `string` | `'code-pushup'` | Name of command to run Code PushUp per project in [monorepo mode](#monorepo-mode) | -| `nxProjectsFilter` | `string \| string[]` | `'--with-target={task}'` | Arguments passed to [`nx show projects`](https://nx.dev/nx-api/nx/documents/show#projects), only relevant for Nx in [monorepo mode](#monorepo-mode) [^2] | -| `directory` | `string` | `process.cwd()` | Directory in which Code PushUp CLI should run | -| `config` | `string \| null` | `null` [^1] | Path to config file (`--config` option) | -| `silent` | `boolean` | `false` | Hides logs from CLI commands (errors will be printed) | -| `bin` | `string` | `'npx --no-install code-pushup'` | Command for executing Code PushUp CLI | -| `detectNewIssues` | `boolean` | `true` | Toggles if new issues should be detected and returned in `newIssues` property | -| `skipComment` | `boolean` | `false` | Toggles if comparison comment is posted to PR | -| `configPatterns` | `ConfigPatterns \| null` | `null` | Additional configuration which enables [faster CI runs](#faster-ci-runs-with-configpatterns) | -| `searchCommits` | `boolean \| number` | `false` | If base branch has no cached report in portal, [extends search up to 100 recent commits](#search-latest-commits-for-previous-report) | +| Property | Type | Default | Description | +| :----------------- | :------------------------- | :------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `monorepo` | `boolean \| MonorepoTool` | `false` | Enables [monorepo mode](#monorepo-mode) | +| `parallel` | `boolean \| number` | `false` | Enables parallel execution in [monorepo mode](#monorepo-mode) | +| `projects` | `string[] \| null` | `null` | Custom projects configuration for [monorepo mode](#monorepo-mode) | +| `task` | `string` | `'code-pushup'` | Name of command to run Code PushUp per project in [monorepo mode](#monorepo-mode) | +| `nxProjectsFilter` | `string \| string[]` | `'--with-target={task}'` | Arguments passed to [`nx show projects`](https://nx.dev/nx-api/nx/documents/show#projects), only relevant for Nx in [monorepo mode](#monorepo-mode) [^2] | +| `directory` | `string` | `process.cwd()` | Directory in which Code PushUp CLI should run | +| `config` | `string \| null` | `null` [^1] | Path to config file (`--config` option) | +| `silent` | `boolean` | `false` | Hides logs from CLI commands (errors will be printed) | +| `bin` | `string` | `'npx --no-install code-pushup'` | Command for executing Code PushUp CLI | +| `detectNewIssues` | `boolean` | `true` | Toggles if new issues should be detected and returned in `newIssues` property | +| `skipComment` | `boolean` | `false` | Toggles if comparison comment is posted to PR | +| `configPatterns` | `ConfigPatterns \| null` | `null` | Additional configuration which enables [faster CI runs](#faster-ci-runs-with-configpatterns) | +| `searchCommits` | `boolean \| number` | `false` | If base branch has no cached report in portal, [extends search up to 100 recent commits](#search-latest-commits-for-previous-report) | +| `jobId` | `string \| number \| null` | `null` | Differentiate PR comments (useful if multiple jobs run Code PushUp) | [^1]: By default, the `code-pushup.config` file is autodetected as described in [`@code-pushup/cli` docs](../cli/README.md#configuration). diff --git a/packages/ci/src/lib/cli/context.unit.test.ts b/packages/ci/src/lib/cli/context.unit.test.ts index dcbd809bb..a95707361 100644 --- a/packages/ci/src/lib/cli/context.unit.test.ts +++ b/packages/ci/src/lib/cli/context.unit.test.ts @@ -1,4 +1,5 @@ import { expect } from 'vitest'; +import { DEFAULT_SETTINGS } from '../settings.js'; import { type CommandContext, createCommandContext } from './context.js'; describe('createCommandContext', () => { @@ -6,19 +7,11 @@ describe('createCommandContext', () => { expect( createCommandContext( { + ...DEFAULT_SETTINGS, bin: 'npx --no-install code-pushup', config: null, - detectNewIssues: true, directory: '/test', silent: false, - monorepo: false, - parallel: false, - nxProjectsFilter: '--with-target={task}', - projects: null, - task: 'code-pushup', - skipComment: false, - configPatterns: null, - searchCommits: false, }, null, ), @@ -34,19 +27,11 @@ describe('createCommandContext', () => { expect( createCommandContext( { + ...DEFAULT_SETTINGS, bin: 'npx --no-install code-pushup', config: null, - detectNewIssues: true, directory: '/test', silent: false, - monorepo: false, - parallel: false, - nxProjectsFilter: '--with-target={task}', - projects: null, - task: 'code-pushup', - skipComment: false, - configPatterns: null, - searchCommits: false, }, { name: 'ui', diff --git a/packages/ci/src/lib/comment.ts b/packages/ci/src/lib/comment.ts index 3f1b7e377..0877785ca 100644 --- a/packages/ci/src/lib/comment.ts +++ b/packages/ci/src/lib/comment.ts @@ -1,13 +1,16 @@ import { readFile } from 'node:fs/promises'; import { logger } from '@code-pushup/utils'; -import type { ProviderAPIClient } from './models.js'; +import type { ProviderAPIClient, Settings } from './models.js'; export async function commentOnPR( mdPath: string, api: ProviderAPIClient, + settings: Pick, ): Promise { const markdown = await readFile(mdPath, 'utf8'); - const identifier = ``; + const identifier = settings.jobId + ? `` + : ''; const body = truncateBody( `${markdown}\n\n${identifier}\n`, api.maxCommentChars, diff --git a/packages/ci/src/lib/comment.unit.test.ts b/packages/ci/src/lib/comment.unit.test.ts index 95022c7e0..e18aa061c 100644 --- a/packages/ci/src/lib/comment.unit.test.ts +++ b/packages/ci/src/lib/comment.unit.test.ts @@ -29,6 +29,8 @@ describe('commentOnPR', () => { listComments: vi.fn(), } satisfies ProviderAPIClient; + const settings = { jobId: null }; + beforeEach(() => { vol.fromJSON({ [diffFile]: diffText }, MEMFS_VOLUME); api.listComments.mockResolvedValue([]); @@ -37,7 +39,9 @@ describe('commentOnPR', () => { it('should create new comment if none existing', async () => { api.listComments.mockResolvedValue([]); - await expect(commentOnPR(diffPath, api)).resolves.toBe(comment.id); + await expect(commentOnPR(diffPath, api, settings)).resolves.toBe( + comment.id, + ); expect(api.listComments).toHaveBeenCalled(); expect(api.createComment).toHaveBeenCalledWith(comment.body); @@ -47,7 +51,9 @@ describe('commentOnPR', () => { it("should create new comment if existing comments don't match", async () => { api.listComments.mockResolvedValue([otherComment]); - await expect(commentOnPR(diffPath, api)).resolves.toBe(comment.id); + await expect(commentOnPR(diffPath, api, settings)).resolves.toBe( + comment.id, + ); expect(api.listComments).toHaveBeenCalled(); expect(api.createComment).toHaveBeenCalledWith(comment.body); @@ -57,17 +63,35 @@ describe('commentOnPR', () => { it('should update previous comment if it matches', async () => { api.listComments.mockResolvedValue([comment]); - await expect(commentOnPR(diffPath, api)).resolves.toBe(comment.id); + await expect(commentOnPR(diffPath, api, settings)).resolves.toBe( + comment.id, + ); expect(api.listComments).toHaveBeenCalled(); expect(api.createComment).not.toHaveBeenCalled(); expect(api.updateComment).toHaveBeenCalledWith(comment.id, comment.body); }); + it('should not match comment with different jobId', async () => { + api.listComments.mockResolvedValue([comment]); + + await expect( + commentOnPR(diffPath, api, { jobId: 'monorepo-mode' }), + ).resolves.toBe(comment.id); + + expect(api.listComments).toHaveBeenCalled(); + expect(api.createComment).toHaveBeenCalledWith( + `${diffText}\n\n\n`, + ); + expect(api.updateComment).not.toHaveBeenCalled(); + }); + it('should update previous comment which matches and ignore other comments', async () => { api.listComments.mockResolvedValue([otherComment, comment]); - await expect(commentOnPR(diffPath, api)).resolves.toBe(comment.id); + await expect(commentOnPR(diffPath, api, settings)).resolves.toBe( + comment.id, + ); expect(api.listComments).toHaveBeenCalled(); expect(api.createComment).not.toHaveBeenCalled(); @@ -80,7 +104,9 @@ describe('commentOnPR', () => { .join('\n'); await writeFile(diffPath, longDiffText); - await expect(commentOnPR(diffPath, api)).resolves.toBe(comment.id); + await expect(commentOnPR(diffPath, api, settings)).resolves.toBe( + comment.id, + ); expect(api.createComment).toHaveBeenCalledWith( expect.stringContaining('...*[Comment body truncated]*'), diff --git a/packages/ci/src/lib/models.ts b/packages/ci/src/lib/models.ts index 51efb6d31..e6ec3db1d 100644 --- a/packages/ci/src/lib/models.ts +++ b/packages/ci/src/lib/models.ts @@ -20,6 +20,7 @@ export type Options = { skipComment?: boolean; configPatterns?: ConfigPatterns | null; searchCommits?: boolean | number; + jobId?: string | number | null; }; /** diff --git a/packages/ci/src/lib/run-monorepo.ts b/packages/ci/src/lib/run-monorepo.ts index de0c53fa7..f3fd83a06 100644 --- a/packages/ci/src/lib/run-monorepo.ts +++ b/packages/ci/src/lib/run-monorepo.ts @@ -81,7 +81,7 @@ export async function runInMonorepoMode( const commentId = settings.skipComment ? null - : await commentOnPR(diffPath, api); + : await commentOnPR(diffPath, api, settings); return { mode: 'monorepo', diff --git a/packages/ci/src/lib/run-standalone.ts b/packages/ci/src/lib/run-standalone.ts index cca6c5a50..9c49aa81f 100644 --- a/packages/ci/src/lib/run-standalone.ts +++ b/packages/ci/src/lib/run-standalone.ts @@ -14,7 +14,7 @@ export async function runInStandaloneMode( const commentMdPath = files.comparison?.md; if (!settings.skipComment && commentMdPath) { - const commentId = await commentOnPR(commentMdPath, api); + const commentId = await commentOnPR(commentMdPath, api, settings); return { mode: 'standalone', files, diff --git a/packages/ci/src/lib/settings.ts b/packages/ci/src/lib/settings.ts index 70a055b6e..e0b13efe0 100644 --- a/packages/ci/src/lib/settings.ts +++ b/packages/ci/src/lib/settings.ts @@ -16,6 +16,7 @@ export const DEFAULT_SETTINGS: Settings = { skipComment: false, configPatterns: null, searchCommits: false, + jobId: null, }; export const MIN_SEARCH_COMMITS = 1; From b47aa6a35d7ccc2e64bf9e0f14ba8400cd9afde0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Fri, 28 Nov 2025 16:01:52 +0100 Subject: [PATCH 15/16] ci(local-action): separate standalone and monorepo code-pushup jobs --- .github/actions/code-pushup/action.yml | 4 +++ .github/actions/code-pushup/src/runner.ts | 43 ++++++++++++++--------- .github/workflows/code-pushup-fork.yml | 31 ++++++++++++++-- .github/workflows/code-pushup.yml | 32 +++++++++++++++-- 4 files changed, 88 insertions(+), 22 deletions(-) diff --git a/.github/actions/code-pushup/action.yml b/.github/actions/code-pushup/action.yml index 82e759bf5..4faf1fb21 100644 --- a/.github/actions/code-pushup/action.yml +++ b/.github/actions/code-pushup/action.yml @@ -6,6 +6,9 @@ inputs: description: GitHub token for API access required: true default: ${{ github.token }} + mode: + description: Is `standalone` or `monorepo` mode? + required: true runs: using: composite @@ -16,3 +19,4 @@ runs: env: TSX_TSCONFIG_PATH: .github/actions/code-pushup/tsconfig.json GH_TOKEN: ${{ inputs.token }} + MODE: ${{ inputs.mode }} diff --git a/.github/actions/code-pushup/src/runner.ts b/.github/actions/code-pushup/src/runner.ts index 95463c2ea..f1fbbbc87 100644 --- a/.github/actions/code-pushup/src/runner.ts +++ b/.github/actions/code-pushup/src/runner.ts @@ -87,7 +87,7 @@ function createAnnotationsFromIssues(issues: SourceFileIssue[]): void { } function createGitHubApiClient(): ProviderAPIClient { - const token = process.env.GH_TOKEN; + const token = process.env['GH_TOKEN']; if (!token) { throw new Error('No GitHub token found'); @@ -135,23 +135,32 @@ async function run(): Promise { logger.setVerbose(true); } - const options: Options = { - monorepo: true, - configPatterns: { - persist: { - ...DEFAULT_PERSIST_CONFIG, - outputDir: '.code-pushup/{projectName}', - }, - ...(process.env['CP_API_KEY'] && { - upload: { - server: 'https://api.staging.code-pushup.dev/graphql', - apiKey: process.env['CP_API_KEY'], - organization: 'code-pushup', - project: 'cli-{projectName}', + const isMonorepo = process.env['MODE'] === 'monorepo'; + + const options: Options = isMonorepo + ? { + jobId: 'monorepo-mode', + monorepo: 'nx', + nxProjectsFilter: '--with-target=code-pushup --exclude=workspace', + configPatterns: { + persist: { + ...DEFAULT_PERSIST_CONFIG, + outputDir: '.code-pushup/{projectName}', + }, + ...(process.env['CP_API_KEY'] && { + upload: { + server: 'https://api.staging.code-pushup.dev/graphql', + apiKey: process.env['CP_API_KEY'], + organization: 'code-pushup', + project: 'cli-{projectName}', + }, + }), }, - }), - }, - }; + } + : { + jobId: 'standalone-mode', + bin: 'npx nx code-pushup --', + }; const gitRefs = parseGitRefs(); diff --git a/.github/workflows/code-pushup-fork.yml b/.github/workflows/code-pushup-fork.yml index 0b920491d..b747702de 100644 --- a/.github/workflows/code-pushup-fork.yml +++ b/.github/workflows/code-pushup-fork.yml @@ -1,4 +1,4 @@ -name: Code PushUp - Standalone Mode (fork) +name: Code PushUp (fork) # separated from code-pushup.yml for security reasons # => requires permissions to create PR comment @@ -18,9 +18,9 @@ permissions: pull-requests: write jobs: - code-pushup: + standalone: runs-on: ubuntu-latest - name: Run Code PushUp (fork) + name: Standalone mode (fork) if: github.event.pull_request.head.repo.fork steps: - name: Checkout repository @@ -40,3 +40,28 @@ jobs: uses: ./.github/actions/code-pushup with: token: ${{ secrets.GITHUB_TOKEN }} + mode: standalone + + monorepo: + runs-on: ubuntu-latest + name: Monorepo mode (fork) + if: github.event.pull_request.head.repo.fork + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: npm + - name: Set base and head for Nx affected commands + uses: nrwl/nx-set-shas@v4 + - name: Install dependencies + run: npm ci + - name: Run Code PushUp action + uses: ./.github/actions/code-pushup + with: + token: ${{ secrets.GITHUB_TOKEN }} + mode: monorepo diff --git a/.github/workflows/code-pushup.yml b/.github/workflows/code-pushup.yml index 7ed679c14..f582753eb 100644 --- a/.github/workflows/code-pushup.yml +++ b/.github/workflows/code-pushup.yml @@ -14,9 +14,9 @@ permissions: pull-requests: write jobs: - code-pushup: + standalone: runs-on: ubuntu-latest - name: Run Code PushUp + name: Standalone mode # ignore PRs from forks, handled by code-pushup-fork.yml if: ${{ !github.event.pull_request.head.repo.fork }} env: @@ -39,3 +39,31 @@ jobs: uses: ./.github/actions/code-pushup with: token: ${{ secrets.GITHUB_TOKEN }} + mode: standalone + + monorepo: + runs-on: ubuntu-latest + name: Monorepo mode + # ignore PRs from forks, handled by code-pushup-fork.yml + if: ${{ !github.event.pull_request.head.repo.fork }} + env: + CP_API_KEY: ${{ secrets.CP_API_KEY }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: npm + - name: Set base and head for Nx affected commands + uses: nrwl/nx-set-shas@v4 + - name: Install dependencies + run: npm ci + - name: Run Code PushUp action + uses: ./.github/actions/code-pushup + with: + token: ${{ secrets.GITHUB_TOKEN }} + mode: monorepo From a61f9917ff408fb2ef3250aeccf542e85b3e0faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Chalk?= Date: Fri, 28 Nov 2025 18:25:23 +0100 Subject: [PATCH 16/16] fix(cli): handle multiple --persist.outputDir arguments --- .../implementation/core-config.int.test.ts | 4 +- packages/cli/src/lib/yargs-cli.int.test.ts | 8 +++ packages/cli/src/lib/yargs-cli.ts | 67 +++++++++++++++++-- packages/utils/src/index.ts | 1 + packages/utils/src/lib/guards.ts | 4 ++ packages/utils/src/lib/guards.unit.test.ts | 27 ++++++++ 6 files changed, 105 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/lib/implementation/core-config.int.test.ts b/packages/cli/src/lib/implementation/core-config.int.test.ts index 4baec9526..7c0a156d1 100644 --- a/packages/cli/src/lib/implementation/core-config.int.test.ts +++ b/packages/cli/src/lib/implementation/core-config.int.test.ts @@ -18,7 +18,7 @@ vi.mock('@code-pushup/core', async () => { return { ...(core as object), readRcByPath: vi.fn().mockImplementation((filepath: string): CoreConfig => { - const allPersistOptions = { + const allPersistOptions: CoreConfig = { ...CORE_CONFIG_MOCK, persist: { filename: 'rc-filename', @@ -26,7 +26,7 @@ vi.mock('@code-pushup/core', async () => { outputDir: 'rc-outputDir', }, }; - const persistOnlyFilename = { + const persistOnlyFilename: CoreConfig = { ...CORE_CONFIG_MOCK, persist: { filename: 'rc-filename', diff --git a/packages/cli/src/lib/yargs-cli.int.test.ts b/packages/cli/src/lib/yargs-cli.int.test.ts index 248e8ff10..cfa097488 100644 --- a/packages/cli/src/lib/yargs-cli.int.test.ts +++ b/packages/cli/src/lib/yargs-cli.int.test.ts @@ -103,6 +103,14 @@ describe('yargsCli', () => { expect(parsedArgv.config).toBe('./config.b.ts'); }); + it('should use the last occurrence of an argument if persist.outputDir is passed multiple times', async () => { + const parsedArgv = await yargsCli>( + ['--persist.outputDir=output-a', '--persist.outputDir=output-b'], + { options }, + ).parseAsync(); + expect(parsedArgv.persist!.outputDir).toBe('output-b'); + }); + it('should ignore unknown options', async () => { const parsedArgv = await yargsCli( ['--no-progress', '--verbose'], diff --git a/packages/cli/src/lib/yargs-cli.ts b/packages/cli/src/lib/yargs-cli.ts index e8c8fdcff..c6868d579 100644 --- a/packages/cli/src/lib/yargs-cli.ts +++ b/packages/cli/src/lib/yargs-cli.ts @@ -13,7 +13,7 @@ import { formatSchema, validate, } from '@code-pushup/models'; -import { TERMINAL_WIDTH } from '@code-pushup/utils'; +import { TERMINAL_WIDTH, isRecord } from '@code-pushup/utils'; import { descriptionStyle, formatNestedValues, @@ -88,11 +88,11 @@ export function yargsCli( .parserConfiguration({ 'strip-dashed': true, } satisfies Partial) - .coerce('config', (config: string | string[]) => - Array.isArray(config) ? config.at(-1) : config, - ) .options(formatNestedValues(options, 'describe')); + // use last argument for non-array options + coerceArraysByOptionType(cli, options); + // usage message if (usageMessage) { cli.usage(titleStyle(usageMessage)); @@ -166,3 +166,62 @@ function validatePersistFormat(persist: PersistConfig) { ); } } + +function coerceArraysByOptionType( + cli: Argv, + options: Record, +): void { + Object.entries(groupOptionsByKey(options)).forEach(([key, node]) => { + cli.coerce(key, (value: unknown) => coerceNode(node, value)); + }); +} + +function coerceNode( + node: OptionsTreeNode | OptionsTreeLeaf, + value: unknown, +): unknown { + if (node.isLeaf) { + if (node.options.type === 'array') { + return node.options.coerce?.(value) ?? value; + } + return Array.isArray(value) ? value.at(-1) : value; + } + return Object.entries(node.children).reduce(coerceChildNode, value); +} + +function coerceChildNode( + value: unknown, + [key, node]: [string, OptionsTreeNode | OptionsTreeLeaf], +): unknown { + if (!isRecord(value) || !(key in value)) { + return value; + } + return { ...value, [key]: coerceNode(node, value[key]) }; +} + +type OptionsTree = Record; +type OptionsTreeNode = { isLeaf: false; children: OptionsTree }; +type OptionsTreeLeaf = { isLeaf: true; options: Options }; + +function groupOptionsByKey(options: Record): OptionsTree { + return Object.entries(options).reduce(addOptionToTree, {}); +} + +function addOptionToTree( + tree: OptionsTree, + [key, value]: [string, Options], +): OptionsTree { + if (!key.includes('.')) { + return { ...tree, [key]: { isLeaf: true, options: value } }; + } + const [parentKey, childKey] = key.split('.', 2) as [string, string]; + const prevChildren = + tree[parentKey] && !tree[parentKey].isLeaf ? tree[parentKey].children : {}; + return { + ...tree, + [parentKey]: { + isLeaf: false, + children: addOptionToTree(prevChildren, [childKey, value]), + }, + }; +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index d34d4ed19..e1fba9c1b 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -84,6 +84,7 @@ export { hasNoNullableProps, isPromiseFulfilledResult, isPromiseRejectedResult, + isRecord, } from './lib/guards.js'; export { interpolate } from './lib/interpolate.js'; export { logMultipleResults } from './lib/log-results.js'; diff --git a/packages/utils/src/lib/guards.ts b/packages/utils/src/lib/guards.ts index aca4ceef0..45a388ec3 100644 --- a/packages/utils/src/lib/guards.ts +++ b/packages/utils/src/lib/guards.ts @@ -17,3 +17,7 @@ export function hasNoNullableProps( ): obj is ExcludeNullableProps { return Object.values(obj).every(value => value != null); } + +export function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value != null; +} diff --git a/packages/utils/src/lib/guards.unit.test.ts b/packages/utils/src/lib/guards.unit.test.ts index 6ad76d058..e76418939 100644 --- a/packages/utils/src/lib/guards.unit.test.ts +++ b/packages/utils/src/lib/guards.unit.test.ts @@ -3,6 +3,7 @@ import { hasNoNullableProps, isPromiseFulfilledResult, isPromiseRejectedResult, + isRecord, } from './guards.js'; describe('promise-result', () => { @@ -42,3 +43,29 @@ describe('hasNoNullableProps', () => { expect(hasNoNullableProps({})).toBeTrue(); }); }); + +describe('isRecord', () => { + it('should return true for an object', () => { + expect(isRecord({ foo: 'bar' })).toBeTrue(); + }); + + it('should return true for an empty object', () => { + expect(isRecord({})).toBeTrue(); + }); + + it('should return true for an array', () => { + expect(isRecord([1, 2, 3])).toBeTrue(); + }); + + it('should return false for a string', () => { + expect(isRecord('foo')).toBeFalse(); + }); + + it('should return false for null', () => { + expect(isRecord(null)).toBeFalse(); + }); + + it('should return false for undefined', () => { + expect(isRecord(undefined)).toBeFalse(); + }); +});