diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 47b928e0f..e9ca5cfbf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -12,5 +12,6 @@ mocks/ @Tlacenka # Plugins /packages/plugin-eslint/ @matejchalk /packages/plugin-coverage/ @Tlacenka +/packages/plugin-js-packages/ @Tlacenka /packages/plugin-lighthouse/ @BioPhoton /examples/plugins/ @BioPhoton diff --git a/.github/labeler.yml b/.github/labeler.yml index 910d00f7b..1b2420209 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -26,6 +26,10 @@ - changed-files: - any-glob-to-any-file: 'packages/plugin-coverage/src/**' +🧩 js-packages-plugin: + - changed-files: + - any-glob-to-any-file: 'packages/plugin-js-packages/src/**' + 🧩 utils: - changed-files: - any-glob-to-any-file: 'packages/utils/src/**' diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 96298ee87..4865fae94 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -12,7 +12,14 @@ jobs: strategy: fail-fast: false matrix: - lib: [cli, core, models, utils, plugin-eslint, plugin-coverage] + lib: + - cli + - core + - models + - utils + - plugin-eslint + - plugin-coverage + - plugin-js-packages scope: [unit, integration] name: Update code coverage runs-on: ubuntu-latest diff --git a/README.md b/README.md index 0ead0ed89..8c8c2f811 100644 --- a/README.md +++ b/README.md @@ -30,5 +30,6 @@ This monorepo contains code for open-source Code PushUp NPM packages: - plugins: - [📦 @code-pushup/eslint-plugin](./packages/plugin-eslint#readme) - static analysis using **ESLint** rules - [📦 @code-pushup/coverage-plugin](./packages/plugin-coverage#readme) - code coverage analysis + - [📦 @code-pushup/js-packages-plugin](./packages/plugin-js-packages#readme) - package audit and outdated dependencies If you want to contribute, please refer to [CONTRIBUTING.md](./CONTRIBUTING.md). diff --git a/e2e/cli-e2e/project.json b/e2e/cli-e2e/project.json index 2e7903951..643248169 100644 --- a/e2e/cli-e2e/project.json +++ b/e2e/cli-e2e/project.json @@ -23,6 +23,7 @@ "plugin-eslint", "plugin-lighthouse", "plugin-coverage", + "plugin-js-packages", "react-todos-app" ], "tags": ["scope:core", "scope:plugin", "type:e2e"] diff --git a/packages/plugin-js-packages/.eslintrc.json b/packages/plugin-js-packages/.eslintrc.json new file mode 100644 index 000000000..cbc1f6668 --- /dev/null +++ b/packages/plugin-js-packages/.eslintrc.json @@ -0,0 +1,19 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "parserOptions": { + "project": ["packages/plugin-js-packages/tsconfig.*?.json"] + } + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": ["error"] + } + } + ] +} diff --git a/packages/plugin-js-packages/README.md b/packages/plugin-js-packages/README.md new file mode 100644 index 000000000..dacfff66f --- /dev/null +++ b/packages/plugin-js-packages/README.md @@ -0,0 +1,160 @@ +# @code-pushup/js-packages-plugin + +[![npm](https://img.shields.io/npm/v/%40code-pushup%2Fjs-packages-plugin.svg)](https://www.npmjs.com/package/@code-pushup/js-packages-plugin) +[![downloads](https://img.shields.io/npm/dm/%40code-pushup%2Fjs-packages-plugin)](https://npmtrends.com/@code-pushup/js-packages-plugin) +[![dependencies](https://img.shields.io/librariesio/release/npm/%40code-pushup/js-packages-plugin)](https://www.npmjs.com/package/@code-pushup/js-packages-plugin?activeTab=dependencies) + +📦 **Code PushUp plugin for JavaScript packages.** 🛡️ + +This plugin checks for known vulnerabilities and outdated dependencies. +It supports the following package managers: + +- [NPM](https://docs.npmjs.com/) +- [Yarn v1](https://classic.yarnpkg.com/docs/) & [Yarn v2+](https://yarnpkg.com/getting-started) +- [PNPM](https://pnpm.io/pnpm-cli) + +## Getting started + +1. If you haven't already, install [@code-pushup/cli](../cli/README.md) and create a configuration file. + +2. Insert plugin configuration with your package manager. By default, both `audit` and `outdated` checks will be run. The result should look as follows: + + ```js + import jsPackagesPlugin from '@code-pushup/js-packages-plugin'; + + export default { + // ... + plugins: [ + // ... + await jsPackagesPlugin({ packageManager: 'npm' }), // replace with your package manager + ], + }; + ``` + + You may run this plugin with a custom configuration for any supported package manager or command. A custom configuration will look similarly to the following: + + ```js + import jsPackagesPlugin from '@code-pushup/js-packages-plugin'; + + export default { + // ... + plugins: [ + // ... + await jsPackagesPlugin({ packageManager: ['yarn'], checks: ['audit'] }), + ], + }; + ``` + +3. (Optional) Reference individual audits or the provided plugin groups which you wish to include in custom categories (use `npx code-pushup print-config` to list audits and groups). + + 💡 Assign weights based on what influence each command should have on the overall category score (assign weight 0 to only include as extra info, without influencing category score). + + ```js + export default { + // ... + categories: [ + { + slug: 'security', + title: 'Security', + refs: [ + { + type: 'group', + plugin: 'npm-audit', // replace prefix with your package manager + slug: 'js-packages', + weight: 1, + }, + ], + }, + { + slug: 'up-to-date', + title: 'Up-to-date tools', + refs: [ + { + type: 'group', + plugin: 'npm-outdated', // replace prefix with your package manager + slug: 'js-packages', + weight: 1, + }, + // ... + ], + }, + // ... + ], + }; + ``` + +4. Run the CLI with `npx code-pushup collect` and view or upload report (refer to [CLI docs](../cli/README.md)). + +## Plugin architecture + +### Plugin configuration specification + +The plugin accepts the following parameters: + +- `packageManager`: The package manager you are using. Supported values: `npm`, `yarn-classic` (v1), `yarn-modern` (v2+), `pnpm`. +- (optional) `checks`: Array of checks to be run. Supported commands: `audit`, `outdated`. Both are configured by default. +- (optional) `auditLevelMapping`: If you wish to set a custom level of issue severity based on audit vulnerability level, you may do so here. Any omitted values will be filled in by defaults. Audit levels are: `critical`, `high`, `moderate`, `low` and `info`. Issue severities are: `error`, `warn` and `info`. By default the mapping is as follows: `critical` and `high` → `error`; `moderate` and `low` → `warning`; `info` → `info`. + +### Audits and group + +This plugin provides a group per check for a convenient declaration in your config. + +```ts + // ... + categories: [ + { + slug: 'dependencies', + title: 'Package dependencies', + refs: [ + { + type: 'group', + plugin: 'js-packages', + slug: 'npm-audit', // replace prefix with your package manager + weight: 1, + }, + { + type: 'group', + plugin: 'js-packages', + slug: 'npm-outdated', // replace prefix with your package manager + weight: 1, + }, + // ... + ], + }, + // ... + ], +``` + +Each dependency group has its own audit. If you want to check only a subset of dependencies (e.g. run audit and outdated for production dependencies) or assign different weights to them, you can do so in the following way: + +```ts + // ... + categories: [ + { + slug: 'dependencies', + title: 'Package dependencies', + refs: [ + { + type: 'audit', + plugin: 'js-packages', + slug: 'npm-audit-prod', // replace prefix with your package manager + weight: 2, + }, + { + type: 'audit', + plugin: 'js-packages', + slug: 'npm-audit-dev', // replace prefix with your package manager + weight: 1, + }, + { + type: 'audit', + plugin: 'js-packages', + slug: 'npm-outdated-prod', // replace prefix with your package manager + weight: 2, + }, + // ... + ], + }, + // ... + ], +``` diff --git a/packages/plugin-js-packages/package.json b/packages/plugin-js-packages/package.json new file mode 100644 index 000000000..6c9c3f963 --- /dev/null +++ b/packages/plugin-js-packages/package.json @@ -0,0 +1,9 @@ +{ + "name": "@code-pushup/js-packages-plugin", + "version": "0.26.1", + "dependencies": { + "@code-pushup/models": "*", + "@code-pushup/utils": "*", + "zod": "^3.22.4" + } +} diff --git a/packages/plugin-js-packages/project.json b/packages/plugin-js-packages/project.json new file mode 100644 index 000000000..5f3b6d638 --- /dev/null +++ b/packages/plugin-js-packages/project.json @@ -0,0 +1,56 @@ +{ + "name": "plugin-js-packages", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/plugin-js-packages/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/esbuild:esbuild", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/plugin-js-packages", + "main": "packages/plugin-js-packages/src/index.ts", + "tsConfig": "packages/plugin-js-packages/tsconfig.lib.json", + "additionalEntryPoints": ["packages/plugin-js-packages/src/bin.ts"], + "assets": ["packages/plugin-js-packages/*.md"], + "esbuildConfig": "esbuild.config.js" + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": [ + "packages/plugin-js-packages/**/*.ts", + "packages/plugin-js-packages/package.json" + ] + } + }, + "unit-test": { + "executor": "@nx/vite:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "config": "packages/plugin-js-packages/vite.config.unit.ts", + "reportsDirectory": "../../coverage/plugin-js-packages/unit-tests" + } + }, + "integration-test": { + "executor": "@nx/vite:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "config": "packages/plugin-js-packages/vite.config.integration.ts", + "reportsDirectory": "../../coverage/plugin-js-packages/integration-tests" + } + }, + "deploy": { + "options": { + "distFolderPath": "dist/packages/plugin-js-packages" + } + }, + "publish": { + "command": "node tools/scripts/publish.mjs plugin-js-packages {args.ver} {args.tag}", + "dependsOn": ["build"] + } + }, + "tags": ["scope:plugin", "type:feature"] +} diff --git a/packages/plugin-js-packages/src/bin.ts b/packages/plugin-js-packages/src/bin.ts new file mode 100644 index 000000000..be3729ada --- /dev/null +++ b/packages/plugin-js-packages/src/bin.ts @@ -0,0 +1,3 @@ +import { executeRunner } from './lib/runner'; + +executeRunner(); diff --git a/packages/plugin-js-packages/src/index.ts b/packages/plugin-js-packages/src/index.ts new file mode 100644 index 000000000..51f3ced7d --- /dev/null +++ b/packages/plugin-js-packages/src/index.ts @@ -0,0 +1,4 @@ +import { jsPackagesPlugin } from './lib/js-packages-plugin'; + +export default jsPackagesPlugin; +export type { JSPackagesPluginConfig } from './lib/config'; diff --git a/packages/plugin-js-packages/src/lib/config.ts b/packages/plugin-js-packages/src/lib/config.ts new file mode 100644 index 000000000..eb0d4c4d8 --- /dev/null +++ b/packages/plugin-js-packages/src/lib/config.ts @@ -0,0 +1,70 @@ +import { z } from 'zod'; +import { IssueSeverity, issueSeveritySchema } from '@code-pushup/models'; + +const packageCommandSchema = z.enum(['audit', 'outdated']); +export type PackageCommand = z.infer; + +const packageManagerSchema = z.enum([ + 'npm', + 'yarn-classic', + 'yarn-modern', + 'pnpm', +]); +export type PackageManager = z.infer; + +const packageAuditLevelSchema = z.enum([ + 'info', + 'low', + 'moderate', + 'high', + 'critical', +]); +export type PackageAuditLevel = z.infer; + +const defaultAuditLevelMapping: Record = { + critical: 'error', + high: 'error', + moderate: 'warning', + low: 'warning', + info: 'info', +}; + +export function fillAuditLevelMapping( + mapping: Partial>, +): Record { + return { + critical: mapping.critical ?? defaultAuditLevelMapping.critical, + high: mapping.high ?? defaultAuditLevelMapping.high, + moderate: mapping.moderate ?? defaultAuditLevelMapping.moderate, + low: mapping.low ?? defaultAuditLevelMapping.low, + info: mapping.info ?? defaultAuditLevelMapping.info, + }; +} + +export const jsPackagesPluginConfigSchema = z.object({ + checks: z + .array(packageCommandSchema, { + description: + 'Package manager commands to be run. Defaults to both audit and outdated.', + }) + .min(1) + .default(['audit', 'outdated']), + packageManager: packageManagerSchema.describe('Package manager to be used.'), + auditLevelMapping: z + .record(packageAuditLevelSchema, issueSeveritySchema, { + description: + 'Mapping of audit levels to issue severity. Custom mapping or overrides may be entered manually, otherwise has a default preset.', + }) + .default(defaultAuditLevelMapping) + .transform(fillAuditLevelMapping), +}); + +export type JSPackagesPluginConfig = z.input< + typeof jsPackagesPluginConfigSchema +>; + +export type FinalJSPackagesPluginConfig = z.infer< + typeof jsPackagesPluginConfigSchema +>; + +export type PackageDependencyType = 'prod' | 'dev' | 'optional'; diff --git a/packages/plugin-js-packages/src/lib/config.unit.test.ts b/packages/plugin-js-packages/src/lib/config.unit.test.ts new file mode 100644 index 000000000..7bff75e7d --- /dev/null +++ b/packages/plugin-js-packages/src/lib/config.unit.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'vitest'; +import { IssueSeverity } from '@code-pushup/models'; +import { + FinalJSPackagesPluginConfig, + JSPackagesPluginConfig, + PackageAuditLevel, + fillAuditLevelMapping, + jsPackagesPluginConfigSchema, +} from './config'; + +describe('jsPackagesPluginConfigSchema', () => { + it('should accept a JS package configuration with all entities', () => { + expect(() => + jsPackagesPluginConfigSchema.parse({ + auditLevelMapping: { moderate: 'error' }, + checks: ['audit'], + packageManager: 'yarn-classic', + } satisfies JSPackagesPluginConfig), + ).not.toThrow(); + }); + + it('should accept a minimal JS package configuration', () => { + expect(() => + jsPackagesPluginConfigSchema.parse({ + packageManager: 'pnpm', + } satisfies JSPackagesPluginConfig), + ).not.toThrow(); + }); + + it('should fill in default values', () => { + const config = jsPackagesPluginConfigSchema.parse({ + packageManager: 'npm', + }); + expect(config).toEqual({ + checks: ['audit', 'outdated'], + packageManager: 'npm', + auditLevelMapping: { + critical: 'error', + high: 'error', + moderate: 'warning', + low: 'warning', + info: 'info', + }, + }); + }); + + it('should throw for no passed commands', () => { + expect(() => + jsPackagesPluginConfigSchema.parse({ + packageManager: 'yarn-classic', + checks: [], + }), + ).toThrow('too_small'); + }); +}); + +describe('fillAuditLevelMapping', () => { + it('should fill in defaults', () => { + expect(fillAuditLevelMapping({})).toEqual< + Record + >({ + critical: 'error', + high: 'error', + moderate: 'warning', + low: 'warning', + info: 'info', + }); + }); + + it('should override mapping for given values', () => { + expect(fillAuditLevelMapping({ high: 'warning', low: 'info' })).toEqual< + Record + >({ + critical: 'error', + high: 'warning', + moderate: 'warning', + low: 'info', + info: 'info', + }); + }); +}); diff --git a/packages/plugin-js-packages/src/lib/constants.ts b/packages/plugin-js-packages/src/lib/constants.ts new file mode 100644 index 000000000..1353c02b4 --- /dev/null +++ b/packages/plugin-js-packages/src/lib/constants.ts @@ -0,0 +1,43 @@ +import { MaterialIcon } from '@code-pushup/models'; +import { PackageDependencyType, PackageManager } from './config'; + +export const pkgManagerNames: Record = { + npm: 'NPM', + 'yarn-classic': 'Yarn v1', + 'yarn-modern': 'Yarn v2+', + pnpm: 'PNPM', +}; + +export const pkgManagerIcons: Record = { + npm: 'npm', + 'yarn-classic': 'yarn', + 'yarn-modern': 'yarn', + pnpm: 'pnpm', +}; + +export const pkgManagerDocs: Record = { + npm: 'https://docs.npmjs.com/', + 'yarn-classic': 'https://classic.yarnpkg.com/docs/', + 'yarn-modern': 'https://yarnpkg.com/getting-started', + pnpm: 'https://pnpm.io/pnpm-cli', +}; +export const auditDocs: Record = { + npm: 'https://docs.npmjs.com/cli/commands/npm-audit', + 'yarn-classic': 'https://classic.yarnpkg.com/docs/cli/audit', + 'yarn-modern': 'https://yarnpkg.com/cli/npm/audit', + pnpm: 'https://pnpm.io/cli/audit/', +}; + +export const outdatedDocs: Record = { + npm: 'https://docs.npmjs.com/cli/commands/npm-outdated', + 'yarn-classic': 'https://classic.yarnpkg.com/docs/cli/outdated/', + 'yarn-modern': 'https://github.com/mskelton/yarn-plugin-outdated', + pnpm: 'https://pnpm.io/cli/outdated', +}; + +export const dependencyDocs: Record = { + prod: 'https://classic.yarnpkg.com/docs/dependency-types#toc-dependencies', + dev: 'https://classic.yarnpkg.com/docs/dependency-types#toc-devdependencies', + optional: + 'https://classic.yarnpkg.com/docs/dependency-types#toc-optionaldependencies', +}; diff --git a/packages/plugin-js-packages/src/lib/js-packages-plugin.ts b/packages/plugin-js-packages/src/lib/js-packages-plugin.ts new file mode 100644 index 000000000..109cf0cd1 --- /dev/null +++ b/packages/plugin-js-packages/src/lib/js-packages-plugin.ts @@ -0,0 +1,143 @@ +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Audit, Group, PluginConfig } from '@code-pushup/models'; +import { name, version } from '../../package.json'; +import { + JSPackagesPluginConfig, + PackageCommand, + PackageDependencyType, + PackageManager, + jsPackagesPluginConfigSchema, +} from './config'; +import { + auditDocs, + dependencyDocs, + outdatedDocs, + pkgManagerDocs, + pkgManagerIcons, + pkgManagerNames, +} from './constants'; +import { createRunnerConfig } from './runner'; + +/** + * Instantiates Code PushUp JS packages plugin for core config. + * + * @example + * import jsPackagesPlugin from '@code-pushup/js-packages-plugin' + * + * export default { + * // ... core config ... + * plugins: [ + * // ... other plugins ... + * await jsPackagesPlugin({ packageManager: 'npm' }) + * ] + * } + * + * @returns Plugin configuration. + */ + +export async function jsPackagesPlugin( + config: JSPackagesPluginConfig, +): Promise { + const jsPackagesPluginConfig = jsPackagesPluginConfigSchema.parse(config); + const pkgManager = jsPackagesPluginConfig.packageManager; + const checks = [...new Set(jsPackagesPluginConfig.checks)]; + + const runnerScriptPath = join( + fileURLToPath(dirname(import.meta.url)), + 'bin.js', + ); + + return { + slug: 'js-packages', + title: 'Plugin for JS packages', + icon: pkgManagerIcons[pkgManager], + description: + 'This plugin runs audit to uncover vulnerabilities and lists outdated dependencies. It supports npm, yarn classic and berry, pnpm package managers.', + docsUrl: pkgManagerDocs[pkgManager], + packageName: name, + version, + audits: createAudits(pkgManager, checks), + groups: createGroups(pkgManager, checks), + runner: await createRunnerConfig(runnerScriptPath, jsPackagesPluginConfig), + }; +} + +function createGroups( + pkgManager: PackageManager, + checks: PackageCommand[], +): Group[] { + const groups: Record = { + audit: { + slug: `${pkgManager}-audit`, + title: `${pkgManagerNames[pkgManager]} audit`, + description: `Group containing ${pkgManagerNames[pkgManager]} vulnerabilities.`, + docsUrl: auditDocs[pkgManager], + refs: [ + // eslint-disable-next-line no-magic-numbers + { slug: `${pkgManager}-audit-prod`, weight: 8 }, + { slug: `${pkgManager}-audit-dev`, weight: 1 }, + { slug: `${pkgManager}-audit-optional`, weight: 1 }, + ], + }, + outdated: { + slug: `${pkgManager}-outdated`, + title: `${pkgManagerNames[pkgManager]} outdated dependencies`, + description: `Group containing outdated ${pkgManagerNames[pkgManager]} dependencies.`, + docsUrl: outdatedDocs[pkgManager], + refs: [ + // eslint-disable-next-line no-magic-numbers + { slug: `${pkgManager}-outdated-prod`, weight: 8 }, + { slug: `${pkgManager}-outdated-dev`, weight: 1 }, + { slug: `${pkgManager}-outdated-optional`, weight: 1 }, + ], + }, + }; + + return checks.map(check => groups[check]); +} + +function createAudits( + pkgManager: PackageManager, + checks: PackageCommand[], +): Audit[] { + return checks.flatMap(check => [ + { + slug: `${pkgManager}-${check}-prod`, + title: getAuditTitle(pkgManager, check, 'prod'), + description: getAuditDescription(check, 'prod'), + docsUrl: dependencyDocs.prod, + }, + { + slug: `${pkgManager}-${check}-dev`, + title: getAuditTitle(pkgManager, check, 'dev'), + description: getAuditDescription(check, 'dev'), + docsUrl: dependencyDocs.dev, + }, + { + slug: `${pkgManager}-${check}-optional`, + title: getAuditTitle(pkgManager, check, 'optional'), + description: getAuditDescription(check, 'optional'), + docsUrl: dependencyDocs.optional, + }, + ]); +} + +function getAuditTitle( + pkgManager: PackageManager, + check: PackageCommand, + dependencyType: PackageDependencyType, +) { + return check === 'audit' + ? `Vulnerabilities for ${pkgManagerNames[pkgManager]} ${dependencyType} dependencies.` + : `Outdated ${pkgManagerNames[pkgManager]} ${dependencyType} dependencies.`; +} + +function getAuditDescription( + check: PackageCommand, + dependencyType: PackageDependencyType, +) { + return check === 'audit' + ? `Runs security audit on ${dependencyType} dependencies.` + : `Checks for outdated ${dependencyType} dependencies`; +} diff --git a/packages/plugin-js-packages/src/lib/js-packages-plugin.unit.test.ts b/packages/plugin-js-packages/src/lib/js-packages-plugin.unit.test.ts new file mode 100644 index 000000000..0ac13036f --- /dev/null +++ b/packages/plugin-js-packages/src/lib/js-packages-plugin.unit.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from 'vitest'; +import { Group, PluginConfig, RunnerConfig } from '@code-pushup/models'; +import { jsPackagesPlugin } from './js-packages-plugin'; + +vi.mock('./runner/index.ts', () => ({ + createRunnerConfig: vi.fn().mockReturnValue({ + command: 'node', + outputFile: 'runner-output.json', + } satisfies RunnerConfig), +})); + +describe('jsPackagesPlugin', () => { + it('should initialise a JS packages plugin', async () => { + await expect( + jsPackagesPlugin({ packageManager: 'npm', checks: ['outdated'] }), + ).resolves.toStrictEqual( + expect.objectContaining({ + slug: 'js-packages', + title: 'Plugin for JS packages', + audits: expect.any(Array), + groups: expect.any(Array), + runner: expect.any(Object), + }), + ); + }); + + it('should create a group for both audit and outdated when no check configuration is provided', async () => { + await expect( + jsPackagesPlugin({ packageManager: 'npm' }), + ).resolves.toStrictEqual( + expect.objectContaining>({ + groups: [ + expect.objectContaining>({ + slug: 'npm-audit', + }), + expect.objectContaining>({ + slug: 'npm-outdated', + }), + ], + }), + ); + }); + + it('should configure a group based on package manager and chosen check', async () => { + await expect( + jsPackagesPlugin({ packageManager: 'yarn-modern', checks: ['outdated'] }), + ).resolves.toStrictEqual( + expect.objectContaining({ + groups: [ + expect.objectContaining>({ + slug: 'yarn-modern-outdated', + }), + ], + }), + ); + }); + + it('should create an audit for each dependency group', async () => { + await expect( + jsPackagesPlugin({ packageManager: 'yarn-classic', checks: ['audit'] }), + ).resolves.toStrictEqual( + expect.objectContaining({ + audits: [ + expect.objectContaining({ slug: 'yarn-classic-audit-prod' }), + expect.objectContaining({ slug: 'yarn-classic-audit-dev' }), + expect.objectContaining({ slug: 'yarn-classic-audit-optional' }), + ], + groups: [ + expect.objectContaining>({ + slug: 'yarn-classic-audit', + refs: [ + { slug: 'yarn-classic-audit-prod', weight: 8 }, + { slug: 'yarn-classic-audit-dev', weight: 1 }, + { slug: 'yarn-classic-audit-optional', weight: 1 }, + ], + }), + ], + }), + ); + }); + + it('should use an icon that matches the chosen package manager', async () => { + await expect( + jsPackagesPlugin({ packageManager: 'pnpm' }), + ).resolves.toStrictEqual( + expect.objectContaining>({ + icon: 'pnpm', + }), + ); + }); +}); diff --git a/packages/plugin-js-packages/src/lib/runner/constants.ts b/packages/plugin-js-packages/src/lib/runner/constants.ts new file mode 100644 index 000000000..e2bcec21b --- /dev/null +++ b/packages/plugin-js-packages/src/lib/runner/constants.ts @@ -0,0 +1,10 @@ +import { join } from 'node:path'; +import { pluginWorkDir } from '@code-pushup/utils'; + +export const WORKDIR = pluginWorkDir('js-packages'); +export const RUNNER_OUTPUT_PATH = join(WORKDIR, 'runner-output.json'); +export const PLUGIN_CONFIG_PATH = join( + process.cwd(), + WORKDIR, + 'plugin-config.json', +); diff --git a/packages/plugin-js-packages/src/lib/runner/index.ts b/packages/plugin-js-packages/src/lib/runner/index.ts new file mode 100644 index 000000000..f5836cc06 --- /dev/null +++ b/packages/plugin-js-packages/src/lib/runner/index.ts @@ -0,0 +1,24 @@ +import { writeFile } from 'node:fs/promises'; +import { dirname } from 'node:path'; +import { RunnerConfig } from '@code-pushup/models'; +import { ensureDirectoryExists } from '@code-pushup/utils'; +import { FinalJSPackagesPluginConfig } from '../config'; +import { PLUGIN_CONFIG_PATH, RUNNER_OUTPUT_PATH } from './constants'; + +export function executeRunner(): void { + return; +} + +export async function createRunnerConfig( + scriptPath: string, + config: FinalJSPackagesPluginConfig, +): Promise { + await ensureDirectoryExists(dirname(PLUGIN_CONFIG_PATH)); + await writeFile(PLUGIN_CONFIG_PATH, JSON.stringify(config)); + + return { + command: 'node', + args: [scriptPath], + outputFile: RUNNER_OUTPUT_PATH, + }; +} diff --git a/packages/plugin-js-packages/tsconfig.json b/packages/plugin-js-packages/tsconfig.json new file mode 100644 index 000000000..893f9a925 --- /dev/null +++ b/packages/plugin-js-packages/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "types": ["vitest"] + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.test.json" + } + ] +} diff --git a/packages/plugin-js-packages/tsconfig.lib.json b/packages/plugin-js-packages/tsconfig.lib.json new file mode 100644 index 000000000..ef2f7e2b3 --- /dev/null +++ b/packages/plugin-js-packages/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": [ + "vite.config.unit.ts", + "vite.config.integration.ts", + "src/**/*.test.ts", + "src/**/*.mock.ts", + "mocks/**/*.ts" + ] +} diff --git a/packages/plugin-js-packages/tsconfig.test.json b/packages/plugin-js-packages/tsconfig.test.json new file mode 100644 index 000000000..9f29d6bb0 --- /dev/null +++ b/packages/plugin-js-packages/tsconfig.test.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"] + }, + "include": [ + "vite.config.unit.ts", + "vite.config.integration.ts", + "mocks/**/*.ts", + "src/**/*.test.ts" + ] +} diff --git a/packages/plugin-js-packages/vite.config.integration.ts b/packages/plugin-js-packages/vite.config.integration.ts new file mode 100644 index 000000000..386a3c29f --- /dev/null +++ b/packages/plugin-js-packages/vite.config.integration.ts @@ -0,0 +1,28 @@ +/// +import { defineConfig } from 'vite'; +import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases'; + +export default defineConfig({ + cacheDir: '../../node_modules/.vite/plugin-js-packages', + test: { + reporters: ['basic'], + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + alias: tsconfigPathAliases(), + pool: 'threads', + poolOptions: { threads: { singleThread: true } }, + coverage: { + reporter: ['text', 'lcov'], + reportsDirectory: '../../coverage/plugin-js-packages/integration-tests', + }, + environment: 'node', + include: ['src/**/*.integration.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + globalSetup: ['../../global-setup.ts'], + setupFiles: [ + '../../testing/test-setup/src/lib/console.mock.ts', + '../../testing/test-setup/src/lib/reset.mocks.ts', + ], + }, +}); diff --git a/packages/plugin-js-packages/vite.config.unit.ts b/packages/plugin-js-packages/vite.config.unit.ts new file mode 100644 index 000000000..dd834f585 --- /dev/null +++ b/packages/plugin-js-packages/vite.config.unit.ts @@ -0,0 +1,29 @@ +/// +import { defineConfig } from 'vite'; +import { tsconfigPathAliases } from '../../tools/vitest-tsconfig-path-aliases'; + +export default defineConfig({ + cacheDir: '../../node_modules/.vite/plugin-js-packages', + test: { + reporters: ['basic'], + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + alias: tsconfigPathAliases(), + pool: 'threads', + poolOptions: { threads: { singleThread: true } }, + coverage: { + reporter: ['text', 'lcov'], + reportsDirectory: '../../coverage/plugin-js-packages/unit-tests', + }, + environment: 'node', + include: ['src/**/*.unit.test.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + globalSetup: ['../../global-setup.ts'], + setupFiles: [ + '../../testing/test-setup/src/lib/fs.mock.ts', + '../../testing/test-setup/src/lib/console.mock.ts', + '../../testing/test-setup/src/lib/reset.mocks.ts', + ], + }, +}); diff --git a/project.json b/project.json index fe487ca3f..3fdb901c8 100644 --- a/project.json +++ b/project.json @@ -36,6 +36,7 @@ "cli", "plugin-eslint", "plugin-coverage", + "plugin-js-packages", "examples-plugins", "react-todos-app" ], @@ -51,6 +52,7 @@ "cli", "plugin-eslint", "plugin-coverage", + "plugin-js-packages", "examples-plugins", "react-todos-app" ], @@ -66,6 +68,7 @@ "cli", "plugin-eslint", "plugin-coverage", + "plugin-js-packages", "examples-plugins", "react-todos-app" ], diff --git a/tsconfig.base.json b/tsconfig.base.json index 0da435abb..830c30909 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -23,6 +23,9 @@ "@code-pushup/core": ["packages/core/src/index.ts"], "@code-pushup/coverage-plugin": ["packages/plugin-coverage/src/index.ts"], "@code-pushup/eslint-plugin": ["packages/plugin-eslint/src/index.ts"], + "@code-pushup/js-packages-plugin": [ + "packages/plugin-js-packages/src/index.ts" + ], "@code-pushup/lighthouse-plugin": [ "packages/plugin-lighthouse/src/index.ts" ],