diff --git a/packages/plugin-js-packages/README.md b/packages/plugin-js-packages/README.md index 8eb9ca87e..dacfff66f 100644 --- a/packages/plugin-js-packages/README.md +++ b/packages/plugin-js-packages/README.md @@ -6,7 +6,7 @@ 📦 **Code PushUp plugin for JavaScript packages.** 🛡️ -This plugin allows you to list outdated dependencies and run audit for known vulnerabilities. +This plugin checks for known vulnerabilities and outdated dependencies. It supports the following package managers: - [NPM](https://docs.npmjs.com/) @@ -17,9 +17,7 @@ It supports the following package managers: 1. If you haven't already, install [@code-pushup/cli](../cli/README.md) and create a configuration file. -2. Insert plugin configuration. By default, `audit` and `outdated` commands will be run. - - Default configuration will look as follows: +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'; @@ -28,14 +26,12 @@ It supports the following package managers: // ... plugins: [ // ... - await jsPackagesPlugin(), + 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: + 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'; @@ -49,7 +45,7 @@ It supports the following package managers: }; ``` -3. (Optional) Reference individual audits or the provided plugin group which you wish to include in custom categories (use `npx code-pushup print-config` to list audits and groups). +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). @@ -58,17 +54,30 @@ It supports the following package managers: // ... categories: [ { - slug: 'dependencies', - title: 'Package dependencies', + slug: 'security', + title: 'Security', refs: [ { type: 'group', - plugin: 'npm-package-manager', // replace prefix with your package manager + 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, + }, + // ... + ], + }, // ... ], }; @@ -82,16 +91,13 @@ It supports the following package managers: The plugin accepts the following parameters: -- (optional) `packageManager`: The package manager you are using. Supported values: `npm`, `yarn-classic` (v1), `yarn-modern` (v2+), `pnpm`. Default is `npm`. +- `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`. -> [!NOTE] -> All parameters are optional so the plugin can be called with no arguments in the default setting. - ### Audits and group -This plugin provides a group for convenient declaration in your config. When defined this way, all measured coverage type audits have the same weight. +This plugin provides a group per check for a convenient declaration in your config. ```ts // ... @@ -103,7 +109,13 @@ This plugin provides a group for convenient declaration in your config. When def { type: 'group', plugin: 'js-packages', - slug: 'npm-package-manager', // replace prefix with your package manager + 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, }, // ... @@ -113,7 +125,7 @@ This plugin provides a group for convenient declaration in your config. When def ], ``` -Each package manager command still has its own audit. So when you want to include a subset of commands or assign different weights to them, you can do so in the following way: +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 // ... @@ -125,15 +137,21 @@ Each package manager command still has its own audit. So when you want to includ { type: 'audit', plugin: 'js-packages', - slug: 'npm-audit', // replace prefix with your package manager + slug: 'npm-audit-prod', // replace prefix with your package manager weight: 2, }, - { + { type: 'audit', plugin: 'js-packages', - slug: 'npm-outdated', // replace prefix with your package manager + 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/src/lib/config.ts b/packages/plugin-js-packages/src/lib/config.ts index 41fbcd8b3..6cd42f27a 100644 --- a/packages/plugin-js-packages/src/lib/config.ts +++ b/packages/plugin-js-packages/src/lib/config.ts @@ -49,9 +49,7 @@ export const jsPackagesPluginConfigSchema = z.object({ }) .min(1) .default(['audit', 'outdated']), - packageManager: packageManagerSchema - .describe('Package manager to be used. Defaults to npm') - .default('npm'), + packageManager: packageManagerSchema.describe('Package manager to be used.'), auditLevelMapping: z .record(packageAuditLevelSchema, issueSeveritySchema, { description: diff --git a/packages/plugin-js-packages/src/lib/config.unit.test.ts b/packages/plugin-js-packages/src/lib/config.unit.test.ts index 36f82cb57..7bff75e7d 100644 --- a/packages/plugin-js-packages/src/lib/config.unit.test.ts +++ b/packages/plugin-js-packages/src/lib/config.unit.test.ts @@ -20,11 +20,17 @@ describe('jsPackagesPluginConfigSchema', () => { }); it('should accept a minimal JS package configuration', () => { - expect(() => jsPackagesPluginConfigSchema.parse({})).not.toThrow(); + expect(() => + jsPackagesPluginConfigSchema.parse({ + packageManager: 'pnpm', + } satisfies JSPackagesPluginConfig), + ).not.toThrow(); }); it('should fill in default values', () => { - const config = jsPackagesPluginConfigSchema.parse({}); + const config = jsPackagesPluginConfigSchema.parse({ + packageManager: 'npm', + }); expect(config).toEqual({ checks: ['audit', 'outdated'], packageManager: 'npm', @@ -39,9 +45,12 @@ describe('jsPackagesPluginConfigSchema', () => { }); it('should throw for no passed commands', () => { - expect(() => jsPackagesPluginConfigSchema.parse({ checks: [] })).toThrow( - 'too_small', - ); + expect(() => + jsPackagesPluginConfigSchema.parse({ + packageManager: 'yarn-classic', + checks: [], + }), + ).toThrow('too_small'); }); }); diff --git a/packages/plugin-js-packages/src/lib/js-packages-plugin.ts b/packages/plugin-js-packages/src/lib/js-packages-plugin.ts index 056945ea4..52cdd5bff 100644 --- a/packages/plugin-js-packages/src/lib/js-packages-plugin.ts +++ b/packages/plugin-js-packages/src/lib/js-packages-plugin.ts @@ -5,11 +5,13 @@ import { name, version } from '../../package.json'; import { JSPackagesPluginConfig, PackageCommand, + PackageManager, jsPackagesPluginConfigSchema, } from './config'; import { createRunnerConfig } from './runner'; import { auditDocs, + groupDepDocs, outdatedDocs, pkgManagerDocs, pkgManagerIcons, @@ -32,8 +34,9 @@ import { * * @returns Plugin configuration. */ + export async function jsPackagesPlugin( - config: JSPackagesPluginConfig = {}, + config: JSPackagesPluginConfig, ): Promise { const jsPackagesPluginConfig = jsPackagesPluginConfigSchema.parse(config); const pkgManager = jsPackagesPluginConfig.packageManager; @@ -44,43 +47,96 @@ export async function jsPackagesPlugin( 'bin.js', ); - const audits: Record = { + 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: `Lists ${pkgManagerNames[pkgManager]} audit vulnerabilities.`, + 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: `Lists ${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 }, + ], }, }; - const group: Group = { - slug: `${pkgManager}-package-manager`, - title: `${pkgManagerNames[pkgManager]} package manager`, - description: `Group containing both audit and dependencies command audits for the ${pkgManagerNames[pkgManager]} package manager.`, - docsUrl: pkgManagerDocs[pkgManager], - refs: checks.map(check => ({ - slug: `${pkgManager}-${check}`, - weight: 1, - })), - }; + return checks.map(check => groups[check]); +} - 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: checks.map(check => audits[check]), - groups: [group], - runner: await createRunnerConfig(runnerScriptPath, jsPackagesPluginConfig), - }; +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: groupDepDocs[pkgManager], + }, + { + slug: `${pkgManager}-${check}-dev`, + title: getAuditTitle(pkgManager, check, 'dev'), + description: getAuditDescription(check, 'dev'), + docsUrl: groupDepDocs[pkgManager], + }, + { + slug: `${pkgManager}-${check}-optional`, + title: getAuditTitle(pkgManager, check, 'optional'), + description: getAuditDescription(check, 'optional'), + docsUrl: groupDepDocs[pkgManager], + }, + ]); +} + +function getAuditTitle( + pkgManager: PackageManager, + check: PackageCommand, + depGroup: 'prod' | 'dev' | 'optional', +) { + return check === 'audit' + ? `Vulnerabilities for ${pkgManagerNames[pkgManager]} ${depGroup} dependencies.` + : `Outdated ${pkgManagerNames[pkgManager]} ${depGroup} dependencies.`; +} + +function getAuditDescription( + check: PackageCommand, + depGroup: 'prod' | 'dev' | 'optional', +) { + return check === 'audit' + ? `Runs security audit on ${depGroup} dependencies.` + : `Checks for outdated ${depGroup} 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 index 973f4aafb..0ac13036f 100644 --- 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 @@ -24,35 +24,54 @@ describe('jsPackagesPlugin', () => { ); }); - it('should set package manager and commands based on configuration', async () => { + it('should create a group for both audit and outdated when no check configuration is provided', async () => { await expect( - jsPackagesPlugin({ packageManager: 'yarn-classic', checks: ['audit'] }), + 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({ - audits: [expect.objectContaining({ slug: 'yarn-classic-audit' })], groups: [ expect.objectContaining>({ - slug: 'yarn-classic-package-manager', - refs: [{ slug: 'yarn-classic-audit', weight: 1 }], + slug: 'yarn-modern-outdated', }), ], }), ); }); - it('should use npm with both audit and outdated commands when no configuration is provided', async () => { - await expect(jsPackagesPlugin()).resolves.toStrictEqual( - expect.objectContaining>({ + 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: 'npm-audit' }), - expect.objectContaining({ slug: 'npm-outdated' }), + 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: 'npm-package-manager', + slug: 'yarn-classic-audit', refs: [ - { slug: 'npm-audit', weight: 1 }, - { slug: 'npm-outdated', weight: 1 }, + { slug: 'yarn-classic-audit-prod', weight: 8 }, + { slug: 'yarn-classic-audit-dev', weight: 1 }, + { slug: 'yarn-classic-audit-optional', weight: 1 }, ], }), ], diff --git a/packages/plugin-js-packages/src/lib/utils.ts b/packages/plugin-js-packages/src/lib/utils.ts index b219e3992..a2e732a6a 100644 --- a/packages/plugin-js-packages/src/lib/utils.ts +++ b/packages/plugin-js-packages/src/lib/utils.ts @@ -34,3 +34,10 @@ export const outdatedDocs: Record = { 'yarn-modern': 'https://github.com/mskelton/yarn-plugin-outdated', pnpm: 'https://pnpm.io/cli/outdated', }; + +export const groupDepDocs: Record = { + npm: 'https://docs.npmjs.com/cli/using-npm/config#include', + 'yarn-classic': 'https://classic.yarnpkg.com/docs/usage', + 'yarn-modern': 'https://yarnpkg.com/cli/npm/audit#options', + pnpm: 'https://pnpm.io/cli/audit#options', +};