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+

}