feat(plugin-js-packages): add setup wizard binding · code-pushup/cli@96f0312
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+}