Skip to content

Commit 96f0312

Browse files
committed
feat(plugin-js-packages): add setup wizard binding
1 parent 7a1f95a commit 96f0312

File tree

8 files changed

+405
-4
lines changed

8 files changed

+405
-4
lines changed

packages/create-cli/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ Each plugin exposes its own configuration keys that can be passed as CLI argumen
4949
| **`--coverage.continueOnFail`** | `boolean` | `true` | Continue if test command fails |
5050
| **`--coverage.categories`** | `boolean` | `true` | Add Code coverage categories |
5151

52+
#### JS Packages
53+
54+
| Option | Type | Default | Description |
55+
| ------------------------------------ | ---------------------------------------------------------- | ------------- | -------------------------- |
56+
| **`--js-packages.packageManager`** | `'npm'` \| `'yarn-classic'` \| `'yarn-modern'` \| `'pnpm'` | auto-detected | Package manager |
57+
| **`--js-packages.checks`** | `('audit'` \| `'outdated')[]` | both | Checks to run |
58+
| **`--js-packages.dependencyGroups`** | `('prod'` \| `'dev'` \| `'optional')[]` | `prod`, `dev` | Dependency groups |
59+
| **`--js-packages.categories`** | `boolean` | `true` | Add JS packages categories |
60+
5261
### Examples
5362

5463
Run interactively (default):

packages/create-cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"dependencies": {
2929
"@code-pushup/coverage-plugin": "0.121.0",
3030
"@code-pushup/eslint-plugin": "0.121.0",
31+
"@code-pushup/js-packages-plugin": "0.121.0",
3132
"@code-pushup/models": "0.121.0",
3233
"@code-pushup/utils": "0.121.0",
3334
"@inquirer/prompts": "^8.0.0",

packages/create-cli/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import yargs from 'yargs';
33
import { hideBin } from 'yargs/helpers';
44
import { coverageSetupBinding } from '@code-pushup/coverage-plugin';
55
import { eslintSetupBinding } from '@code-pushup/eslint-plugin';
6+
import { jsPackagesSetupBinding } from '@code-pushup/js-packages-plugin';
67
import { parsePluginSlugs, validatePluginSlugs } from './lib/setup/plugins.js';
78
import {
89
CI_PROVIDERS,
@@ -12,10 +13,11 @@ import {
1213
} from './lib/setup/types.js';
1314
import { runSetupWizard } from './lib/setup/wizard.js';
1415

15-
// TODO: create, import and pass remaining plugin bindings (lighthouse, typescript, js-packages, jsdocs, axe)
16+
// TODO: create, import and pass remaining plugin bindings (lighthouse, typescript, jsdocs, axe)
1617
const bindings: PluginSetupBinding[] = [
1718
eslintSetupBinding,
1819
coverageSetupBinding,
20+
jsPackagesSetupBinding,
1921
];
2022

2123
const argv = await yargs(hideBin(process.argv))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { jsPackagesPlugin } from './lib/js-packages-plugin.js';
22

33
export default jsPackagesPlugin;
4+
export { jsPackagesSetupBinding } from './lib/binding.js';
45
export type { JSPackagesPluginConfig } from './lib/config.js';
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { createRequire } from 'node:module';
2+
import path from 'node:path';
3+
import type {
4+
CategoryConfig,
5+
PluginAnswer,
6+
PluginSetupBinding,
7+
} from '@code-pushup/models';
8+
import {
9+
answerArray,
10+
answerBoolean,
11+
answerString,
12+
fileExists,
13+
singleQuote,
14+
} from '@code-pushup/utils';
15+
import type { PackageManagerId } from './config.js';
16+
import {
17+
DEFAULT_CHECKS,
18+
DEFAULT_DEPENDENCY_GROUPS,
19+
JS_PACKAGES_PLUGIN_SLUG,
20+
JS_PACKAGES_PLUGIN_TITLE,
21+
} from './constants.js';
22+
import { derivePackageManager } from './package-managers/derive-package-manager.js';
23+
24+
const { name: PACKAGE_NAME } = createRequire(import.meta.url)(
25+
'../../package.json',
26+
) as typeof import('../../package.json');
27+
28+
const DEFAULT_PACKAGE_MANAGER = 'npm';
29+
30+
const PACKAGE_MANAGERS = [
31+
{ name: 'npm', value: DEFAULT_PACKAGE_MANAGER },
32+
{ name: 'yarn (classic)', value: 'yarn-classic' },
33+
{ name: 'yarn (modern)', value: 'yarn-modern' },
34+
{ name: 'pnpm', value: 'pnpm' },
35+
] as const;
36+
37+
const CHECKS = [
38+
{ name: 'audit (security vulnerabilities)', value: 'audit' },
39+
{ name: 'outdated (outdated dependencies)', value: 'outdated' },
40+
] as const;
41+
42+
const DEPENDENCY_GROUPS = [
43+
{ name: 'production', value: 'prod' },
44+
{ name: 'development', value: 'dev' },
45+
{ name: 'optional', value: 'optional' },
46+
] as const;
47+
48+
const CATEGORIES = [
49+
{
50+
check: 'audit',
51+
slug: 'security',
52+
title: 'Security',
53+
description: 'Finds known **vulnerabilities** in third-party packages.',
54+
},
55+
{
56+
check: 'outdated',
57+
slug: 'updates',
58+
title: 'Updates',
59+
description: 'Finds **outdated** third-party packages.',
60+
},
61+
];
62+
63+
type JsPackagesOptions = {
64+
packageManager: string;
65+
checks: string[];
66+
dependencyGroups: string[];
67+
categories: boolean;
68+
};
69+
70+
export const jsPackagesSetupBinding = {
71+
slug: JS_PACKAGES_PLUGIN_SLUG,
72+
title: JS_PACKAGES_PLUGIN_TITLE,
73+
packageName: PACKAGE_NAME,
74+
isRecommended,
75+
prompts: async (targetDir: string) => {
76+
const packageManager = await detectPackageManager(targetDir);
77+
return [
78+
{
79+
key: 'js-packages.packageManager',
80+
message: 'Package manager',
81+
type: 'select',
82+
choices: [...PACKAGE_MANAGERS],
83+
default: packageManager,
84+
},
85+
{
86+
key: 'js-packages.checks',
87+
message: 'Checks to run',
88+
type: 'checkbox',
89+
choices: [...CHECKS],
90+
default: [...DEFAULT_CHECKS],
91+
},
92+
{
93+
key: 'js-packages.dependencyGroups',
94+
message: 'Dependency groups',
95+
type: 'checkbox',
96+
choices: [...DEPENDENCY_GROUPS],
97+
default: [...DEFAULT_DEPENDENCY_GROUPS],
98+
},
99+
{
100+
key: 'js-packages.categories',
101+
message: 'Add JS packages categories?',
102+
type: 'confirm',
103+
default: true,
104+
},
105+
];
106+
},
107+
generateConfig: (answers: Record<string, PluginAnswer>) => {
108+
const options = parseAnswers(answers);
109+
return {
110+
imports: [
111+
{ moduleSpecifier: PACKAGE_NAME, defaultImport: 'jsPackagesPlugin' },
112+
],
113+
pluginInit: formatPluginInit(options),
114+
...(options.categories ? { categories: createCategories(options) } : {}),
115+
};
116+
},
117+
} satisfies PluginSetupBinding;
118+
119+
function parseAnswers(
120+
answers: Record<string, PluginAnswer>,
121+
): JsPackagesOptions {
122+
return {
123+
packageManager:
124+
answerString(answers, 'js-packages.packageManager') ||
125+
DEFAULT_PACKAGE_MANAGER,
126+
checks: answerArray(answers, 'js-packages.checks'),
127+
dependencyGroups: answerArray(answers, 'js-packages.dependencyGroups'),
128+
categories: answerBoolean(answers, 'js-packages.categories'),
129+
};
130+
}
131+
132+
function formatPluginInit(options: JsPackagesOptions): string[] {
133+
const { packageManager, checks, dependencyGroups } = options;
134+
135+
const hasNonDefaultChecks =
136+
checks.length > 0 && checks.length < DEFAULT_CHECKS.length;
137+
const hasNonDefaultDepGroups =
138+
dependencyGroups.length !== DEFAULT_DEPENDENCY_GROUPS.length ||
139+
!DEFAULT_DEPENDENCY_GROUPS.every(g => dependencyGroups.includes(g));
140+
141+
const body = [
142+
`packageManager: ${singleQuote(packageManager)},`,
143+
hasNonDefaultChecks
144+
? `checks: [${checks.map(singleQuote).join(', ')}],`
145+
: '',
146+
hasNonDefaultDepGroups
147+
? `dependencyGroups: [${dependencyGroups.map(singleQuote).join(', ')}],`
148+
: '',
149+
].filter(Boolean);
150+
151+
return ['await jsPackagesPlugin({', ...body.map(line => ` ${line}`), '}),'];
152+
}
153+
154+
function createCategories({
155+
packageManager,
156+
checks,
157+
}: JsPackagesOptions): CategoryConfig[] {
158+
return CATEGORIES.filter(({ check }) => checks.includes(check)).map(
159+
({ check, slug, title, description }) => ({
160+
slug,
161+
title,
162+
description,
163+
refs: [
164+
{
165+
type: 'group',
166+
plugin: JS_PACKAGES_PLUGIN_SLUG,
167+
slug: `${packageManager}-${check}`,
168+
weight: 1,
169+
},
170+
],
171+
}),
172+
);
173+
}
174+
175+
async function isRecommended(targetDir: string): Promise<boolean> {
176+
return fileExists(path.join(targetDir, 'package.json'));
177+
}
178+
179+
async function detectPackageManager(
180+
targetDir: string,
181+
): Promise<PackageManagerId> {
182+
try {
183+
return await derivePackageManager(targetDir);
184+
} catch {
185+
return DEFAULT_PACKAGE_MANAGER;
186+
}
187+
}

0 commit comments

Comments
 (0)