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 47c5d3dd2..f1fbbbc87 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, @@ -86,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'); @@ -134,9 +135,32 @@ async function run(): Promise { logger.setVerbose(true); } - const options: Options = { - bin: 'npx nx code-pushup --nx-bail --', - }; + 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 9cf5477ca..f582753eb 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: @@ -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 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/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 fda87e20c..99660378b 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, @@ -6,218 +7,229 @@ 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, { 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(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: `cli-${projectName}`, }, + }), + 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: [ + { + 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 }, + ], + }, { - type: 'group', - plugin: 'js-packages', - slug: 'npm-outdated', - 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 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 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: 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), + coverageToolCommand: { + command: `npx nx run-many -t ${targets.join(',')}`, + }, + }; + return { + plugins: [await coveragePlugin(config)], + 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 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 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 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 function configureTypescriptPlugin(projectName?: string): CoreConfig { + const tsconfig = projectName + ? `packages/${projectName}/tsconfig.lib.json` + : 'tsconfig.base.json'; + return { + plugins: [typescriptPlugin({ tsconfig })], + categories: getCategories(), + }; } -export const coverageCategories: CategoryConfig[] = [ - { - slug: 'code-coverage', - title: 'Code coverage', - description: 'Measures how much of your code is **covered by tests**.', - refs: [ +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: [ { - type: 'group', - plugin: 'coverage', - slug: 'coverage', - weight: 1, + 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 const jsPackagesCoreConfig = async (): Promise => ({ - plugins: [await jsPackagesPlugin()], - categories: jsPackagesCategories, -}); + }; +} -export const lighthouseCoreConfig = async ( +export async function configureLighthousePlugin( urls: PluginUrls, -): Promise => { +): 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, lighthouseCategories), + categories: mergeLighthouseCategories(lhPlugin, lhCategories), }; -}; - -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: ['.'], - }) - : 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'], - }, - }), - ], - categories: eslintCategories, -}); - -export const typescriptPluginConfig = async ( - options?: TypescriptPluginOptions, -): Promise => ({ - plugins: [await typescriptPlugin(options)], - categories: getCategories(), -}); - -export const coverageCoreConfigNx = async ( - projectName?: string, -): Promise => { - const targetNames = ['unit-test', 'int-test']; +export function axeCoreConfig(urls: PluginUrls): CoreConfig { 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), - }), - ], - categories: coverageCategories, + plugins: [axePlugin(urls)], }; -}; - -export const axeCoreConfig = (urls: PluginUrls): CoreConfig => ({ - plugins: [axePlugin(urls)], -}); +} 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/nx.json b/nx.json index 80aa82594..648165e0f 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", @@ -16,7 +12,8 @@ "!{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}/code-pushup.config.bundled_*.mjs", "!{projectRoot}/@(test|mocks|mock)/**/*", "!{projectRoot}/**/?(*.)test.[jt]s?(x)?(.snap)", "!{projectRoot}/**/?(*.)mocks.[jt]s?(x)", @@ -28,39 +25,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": { @@ -142,19 +116,14 @@ }, "code-pushup": { "cache": false, - "outputs": [ - "{projectRoot}/.code-pushup/report.md", - "{projectRoot}/.code-pushup/report.json" - ], "executor": "nx:run-commands", + "dependsOn": ["code-pushup-*"], "options": { "command": "node packages/cli/src/index.ts", "args": [ - "--verbose", "--config={projectRoot}/code-pushup.config.ts", "--cache.read", - "--persist.outputDir={projectRoot}/.code-pushup", - "--upload.project=cli-{projectName}" + "--persist.outputDir=.code-pushup/{projectName}" ], "env": { "NODE_OPTIONS": "--import tsx", @@ -163,20 +132,21 @@ } }, "code-pushup-coverage": { + "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", - "dependsOn": ["*-test"], "options": { "command": "node packages/cli/src/index.ts collect", "args": [ - "--verbose", "--config={projectRoot}/code-pushup.config.ts", "--cache.write", "--onlyPlugins=coverage", "--persist.skipReports=true", - "--persist.outputDir={projectRoot}/.code-pushup" + "--persist.outputDir=.code-pushup/{projectName}" ], "env": { "NODE_OPTIONS": "--import tsx", @@ -185,19 +155,21 @@ } }, "code-pushup-eslint": { + "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", "args": [ - "--verbose", "--config={projectRoot}/code-pushup.config.ts", "--cache.write", "--onlyPlugins=eslint", "--persist.skipReports", - "--persist.outputDir={projectRoot}/.code-pushup" + "--persist.outputDir=.code-pushup/{projectName}" ], "env": { "NODE_OPTIONS": "--import tsx", @@ -212,15 +184,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": [ - "--verbose", "--config={projectRoot}/code-pushup.config.ts", "--onlyPlugins=js-packages", - "--persist.outputDir={projectRoot}/.code-pushup" + "--persist.outputDir=.code-pushup/{projectName}" ], "env": { "NODE_OPTIONS": "--import tsx", @@ -231,17 +204,18 @@ "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", "args": [ - "--verbose", "--config={projectRoot}/code-pushup.config.ts", "--cache.write", "--onlyPlugins=lighthouse", "--persist.skipReports", - "--persist.outputDir={projectRoot}/.code-pushup" + "--persist.outputDir=.code-pushup/{projectName}" ], "env": { "NODE_OPTIONS": "--import tsx", @@ -256,17 +230,18 @@ "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", "args": [ - "--verbose", "--config={projectRoot}/code-pushup.config.ts", "--cache.write", "--onlyPlugins=jsdocs", "--persist.skipReports", - "--persist.outputDir={projectRoot}/.code-pushup" + "--persist.outputDir=.code-pushup/{projectName}" ], "env": { "NODE_OPTIONS": "--import tsx", @@ -281,17 +256,18 @@ "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", "args": [ - "--verbose", "--config={projectRoot}/code-pushup.config.ts", "--cache.write", "--onlyPlugins=typescript", "--persist.skipReports", - "--persist.outputDir={projectRoot}/.code-pushup" + "--persist.outputDir=.code-pushup/{projectName}" ], "env": { "NODE_OPTIONS": "--import tsx", @@ -302,16 +278,17 @@ "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", "args": [ - "--verbose", "--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", 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/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..41fd8cda7 100644 --- a/packages/ci/project.json +++ b/packages/ci/project.json @@ -6,9 +6,13 @@ "targets": { "build": {}, "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/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; 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..c92b12115 100644 --- a/packages/cli/project.json +++ b/packages/cli/project.json @@ -6,27 +6,13 @@ "targets": { "build": {}, "lint": {}, - "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/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/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..6ffc72788 100644 --- a/packages/core/project.json +++ b/packages/core/project.json @@ -6,9 +6,13 @@ "targets": { "build": {}, "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..8e4af8b5b 100644 --- a/packages/create-cli/project.json +++ b/packages/create-cli/project.json @@ -6,18 +6,13 @@ "targets": { "build": {}, "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..0045b607e 100644 --- a/packages/models/project.json +++ b/packages/models/project.json @@ -25,8 +25,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..04a76ed34 100644 --- a/packages/plugin-coverage/project.json +++ b/packages/plugin-coverage/project.json @@ -6,9 +6,13 @@ "targets": { "build": {}, "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..beabbc297 100644 --- a/packages/plugin-eslint/project.json +++ b/packages/plugin-eslint/project.json @@ -6,9 +6,13 @@ "targets": { "build": {}, "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..5ef4c7cf1 100644 --- a/packages/plugin-js-packages/project.json +++ b/packages/plugin-js-packages/project.json @@ -6,9 +6,13 @@ "targets": { "build": {}, "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..4fb8fb531 100644 --- a/packages/plugin-jsdocs/project.json +++ b/packages/plugin-jsdocs/project.json @@ -7,8 +7,12 @@ "targets": { "build": {}, "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..5ce9c8643 100644 --- a/packages/plugin-lighthouse/project.json +++ b/packages/plugin-lighthouse/project.json @@ -6,8 +6,12 @@ "targets": { "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/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/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..f6da89dc2 100644 --- a/packages/plugin-typescript/project.json +++ b/packages/plugin-typescript/project.json @@ -6,9 +6,13 @@ "targets": { "build": {}, "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-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, }); 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..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": { @@ -21,6 +20,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": { 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(); + }); +}); diff --git a/project.json b/project.json index 2b378ab31..48e03fd94 100644 --- a/project.json +++ b/project.json @@ -1,42 +1,11 @@ { - "name": "cli-workspace", + "name": "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": [ - "--verbose", - "--cache.read", - "--persist.outputDir={projectRoot}/.code-pushup", - "--upload.project={projectName}" - ] + "args": [] } } }