Skip to content

Commit

Permalink
feat(plugin-js-packages): implement plugin schema and configuration flow
Browse files Browse the repository at this point in the history
  • Loading branch information
Tlacenka committed Mar 11, 2024
1 parent 4020267 commit e2ce3f6
Show file tree
Hide file tree
Showing 10 changed files with 495 additions and 1 deletion.
131 changes: 130 additions & 1 deletion packages/plugin-js-packages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,135 @@
[![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.**
📦 **Code PushUp plugin for JavaScript packages.** 🛡

This plugin allows you to list outdated dependencies and run audit for known vulnerabilities.
It supports the following package managers: npm, yarn, yarn berry, pnpm.

## Getting started

1. If you haven't already, install [@code-pushup/cli](../cli/README.md) and create a configuration file.

2. Insert plugin configuration. By default, npm audit and npm outdated commands will be run.

Default configuration will look as follows:

```js
import jsPackagesPlugin from '@code-pushup/js-packages-plugin';

export default {
// ...
plugins: [
// ...
await jsPackagesPlugin(),
],
};
```

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'], features: ['audit'] }),
],
};
```

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).

💡 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: 'dependencies',
title: 'Package dependencies',
refs: [
{
type: 'group',
plugin: 'npm-package-manager', // 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:

- (optional) `packageManager`: The package manager you are using. Supported values: `npm`, `yarn` (v1), `yarn-berry` (v2+), `pnpm`. Default is `npm`.
- (optional) `features`: Array of commands 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.

```ts
// ...
categories: [
{
slug: 'dependencies',
title: 'Package dependencies',
refs: [
{
type: 'group',
plugin: 'js-packages',
slug: 'npm-package-manager', // replace prefix with your package manager
weight: 1,
},
// ...
],
},
// ...
],
```

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:

```ts
// ...
categories: [
{
slug: 'dependencies',
title: 'Package dependencies',
refs: [
{
type: 'audit',
plugin: 'js-packages',
slug: 'npm-audit', // replace prefix with your package manager
weight: 2,
},
{
type: 'audit',
plugin: 'js-packages',
slug: 'npm-outdated', // replace prefix with your package manager
weight: 1,
},
// ...
],
},
// ...
],
```
1 change: 1 addition & 0 deletions packages/plugin-js-packages/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.26.1",
"dependencies": {
"@code-pushup/models": "*",
"@code-pushup/utils": "*",
"zod": "^3.22.4"
}
}
4 changes: 4 additions & 0 deletions packages/plugin-js-packages/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { jsPackagesPlugin } from './lib/js-packages-plugin';

export default jsPackagesPlugin;
export type { JSPackagesPluginConfig } from './lib/config';
83 changes: 83 additions & 0 deletions packages/plugin-js-packages/src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { z } from 'zod';
import { IssueSeverity, issueSeveritySchema } from '@code-pushup/models';

const packageCommandSchema = z.enum(['audit', 'outdated']);
export type PackageCommand = z.infer<typeof packageCommandSchema>;

const packageManagerSchema = z.enum(['npm', 'yarn', 'yarn-berry', 'pnpm']);
export type PackageManager = z.infer<typeof packageManagerSchema>;

const packageAuditLevelSchema = z.enum([
'info',
'low',
'moderate',
'high',
'critical',
]);
export type PackageAuditLevel = z.infer<typeof packageAuditLevelSchema>;

const defaultAuditLevelMapping: Record<PackageAuditLevel, IssueSeverity> = {
critical: 'error',
high: 'error',
moderate: 'warning',
low: 'warning',
info: 'info',
};

export function fillAuditLevelMapping(
mapping: Partial<Record<PackageAuditLevel, IssueSeverity>>,
): Record<PackageAuditLevel, IssueSeverity> {
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,
};
}

// TODO how?
// export function objectKeys<T extends object>(obj: T): (keyof T)[] {
// return Object.keys(obj) as (keyof T)[];
// }

// function newFillAuditLevelMapping(
// mapping: Partial<Record<PackageAuditLevel, IssueSeverity>>,
// ): Record<PackageAuditLevel, IssueSeverity> {
// return Object.fromEntries(
// objectKeys(defaultAuditLevelMapping).map<
// [PackageAuditLevel, IssueSeverity]
// >(auditLevel => [
// auditLevel,
// mapping[auditLevel] ?? defaultAuditLevelMapping[auditLevel],
// ]),
// );
// }

export const jsPackagesPluginConfigSchema = z.object({
features: 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. Defaults to npm')
.default('npm'),
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
>;
72 changes: 72 additions & 0 deletions packages/plugin-js-packages/src/lib/config.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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' },
features: ['audit'],
packageManager: 'yarn',
} satisfies JSPackagesPluginConfig),
).not.toThrow();
});

it('should accept a minimal JS package configuration', () => {
expect(() => jsPackagesPluginConfigSchema.parse({})).not.toThrow();
});

it('should fill in default values', () => {
const config = jsPackagesPluginConfigSchema.parse({});
expect(config).toEqual<FinalJSPackagesPluginConfig>({
features: ['audit', 'outdated'],
packageManager: 'npm',
auditLevelMapping: {
critical: 'error',
high: 'error',
moderate: 'warning',
low: 'warning',
info: 'info',
},
});
});

it('should throw for no features', () => {
expect(() => jsPackagesPluginConfigSchema.parse({ features: [] })).toThrow(
'too_small',
);
});
});

describe('fillAuditLevelMapping', () => {
it('should fill in defaults', () => {
expect(fillAuditLevelMapping({})).toEqual<
Record<PackageAuditLevel, IssueSeverity>
>({
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<PackageAuditLevel, IssueSeverity>
>({
critical: 'error',
high: 'warning',
moderate: 'warning',
low: 'info',
info: 'info',
});
});
});
81 changes: 81 additions & 0 deletions packages/plugin-js-packages/src/lib/js-packages-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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,
jsPackagesPluginConfigSchema,
} from './config';
import { createRunnerConfig } from './runner';
import { auditDocs, outdatedDocs, pkgManagerDocs } from './utils';

/**
* Instantiates Code PushUp JS packages plugin for core config.
*
* @example
* import coveragePlugin from '@code-pushup/js-packages-plugin'
*
* export default {
* // ... core config ...
* plugins: [
* // ... other plugins ...
* await jsPackagesPlugin()
* ]
* }
*
* @returns Plugin configuration.
*/
export async function jsPackagesPlugin(
config: JSPackagesPluginConfig = {},
): Promise<PluginConfig> {
const jsPackagesPluginConfig = jsPackagesPluginConfigSchema.parse(config);
const pkgManager = jsPackagesPluginConfig.packageManager;
const features = [...new Set(jsPackagesPluginConfig.features)];

const runnerScriptPath = join(
fileURLToPath(dirname(import.meta.url)),
'bin.js',
);

const audits: Record<PackageCommand, Audit> = {
audit: {
slug: `${pkgManager}-audit`,
title: `${pkgManager} audit`,
description: `Lists ${pkgManager} audit vulnerabilities.`,
docsUrl: auditDocs[pkgManager],
},
outdated: {
slug: `${pkgManager}-outdated`,
title: `${pkgManager} outdated dependencies`,
description: `Lists ${pkgManager} outdated dependencies.`,
docsUrl: outdatedDocs[pkgManager],
},
};

const group: Group = {
slug: `${pkgManager}-package-manager`,
title: `${pkgManager} package manager`,
description: `Group containing both audit and dependencies command audits for the ${pkgManager} package manager.`,
docsUrl: pkgManagerDocs[pkgManager],
refs: features.map(feature => ({
slug: `${pkgManager}-${feature}`,
weight: 1,
})),
};

return {
slug: 'js-packages',
title: 'Plugin for JS packages',
icon:
pkgManager === 'npm' ? 'npm' : pkgManager === 'pnpm' ? 'pnpm' : 'yarn',
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: features.map(feature => audits[feature]),
groups: [group],
runner: await createRunnerConfig(runnerScriptPath, jsPackagesPluginConfig),
};
}
Loading

0 comments on commit e2ce3f6

Please sign in to comment.