From 5dcdcd7406f9c085586788f7743121ef57a7b522 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Tue, 3 Mar 2026 14:08:36 -0500 Subject: [PATCH 1/3] feat(create-cli): add plugin selection step --- packages/create-cli/src/index.ts | 6 +- packages/create-cli/src/lib/setup/prompts.ts | 91 ++++++++++- .../src/lib/setup/prompts.unit.test.ts | 142 +++++++++++++++++- packages/create-cli/src/lib/setup/types.ts | 14 +- packages/create-cli/src/lib/setup/wizard.ts | 10 +- 5 files changed, 253 insertions(+), 10 deletions(-) diff --git a/packages/create-cli/src/index.ts b/packages/create-cli/src/index.ts index 48b003882..9908149bd 100755 --- a/packages/create-cli/src/index.ts +++ b/packages/create-cli/src/index.ts @@ -21,7 +21,11 @@ const argv = await yargs(hideBin(process.argv)) choices: CONFIG_FILE_FORMATS, describe: 'Config file format (default: auto-detected from project)', }) + .option('plugins', { + type: 'string', + describe: 'Comma-separated plugin slugs to include (e.g. eslint,coverage)', + }) .parse(); -// TODO: #1244 — provide plugin bindings from registry +// TODO: create, import and pass plugin bindings (eslint, coverage, lighthouse, typescript, js-packages, jsdocs, axe) await runSetupWizard([], argv); diff --git a/packages/create-cli/src/lib/setup/prompts.ts b/packages/create-cli/src/lib/setup/prompts.ts index 053675794..0aff4d81f 100644 --- a/packages/create-cli/src/lib/setup/prompts.ts +++ b/packages/create-cli/src/lib/setup/prompts.ts @@ -1,8 +1,95 @@ import { checkbox, input, select } from '@inquirer/prompts'; import { asyncSequential } from '@code-pushup/utils'; -import type { CliArgs, PluginPromptDescriptor } from './types.js'; +import type { + CliArgs, + PluginPromptDescriptor, + PluginSetupBinding, +} from './types.js'; -// TODO: #1244 — add promptPluginSelection (multi-select prompt with pre-selection callbacks) +/** + * Resolves which plugins to include in the generated config. + * + * Resolution order (first match wins): + * 1. `--plugins` CLI argument: comma-separated slugs, validated against available bindings + * 2. `--yes` flag: recommended plugins (or all if none recommended) + * 3. Interactive: checkbox prompt with recommended plugins pre-checked + */ +export async function promptPluginSelection( + bindings: PluginSetupBinding[], + targetDir: string, + cliArgs: CliArgs, +): Promise { + if (bindings.length === 0) { + return []; + } + const slugs = parsePluginSlugs(cliArgs.plugins); + if (slugs != null) { + return filterBindingsBySlugs(bindings, slugs); + } + const recommended = await detectRecommended(bindings, targetDir); + if (cliArgs.yes) { + return recommended.size > 0 + ? bindings.filter(({ slug }) => recommended.has(slug)) + : bindings; + } + const selected = await checkbox({ + message: 'Plugins to include:', + required: true, + choices: bindings.map(({ title, slug }) => ({ + name: title, + value: slug, + checked: recommended.has(slug), + })), + }); + const selectedSet = new Set(selected); + return bindings.filter(({ slug }) => selectedSet.has(slug)); +} + +function parsePluginSlugs(value: string | undefined): string[] | null { + if (value == null || value.trim() === '') { + return null; + } + return [ + ...new Set( + value + .split(',') + .map(s => s.trim()) + .filter(Boolean), + ), + ]; +} + +function filterBindingsBySlugs( + bindings: PluginSetupBinding[], + slugs: string[], +): PluginSetupBinding[] { + const unknown = slugs.filter(slug => !bindings.some(b => b.slug === slug)); + if (unknown.length > 0) { + throw new Error( + `Unknown plugin slugs: ${unknown.join(', ')}. Available: ${bindings.map(b => b.slug).join(', ')}`, + ); + } + return bindings.filter(b => slugs.includes(b.slug)); +} + +/** + * Calls each binding's `isRecommended` callback (if provided) + * and collects the slugs of bindings that returned `true`. + */ +async function detectRecommended( + bindings: PluginSetupBinding[], + targetDir: string, +): Promise> { + const recommended = new Set(); + await Promise.all( + bindings.map(async ({ slug, isRecommended }) => { + if (isRecommended && (await isRecommended(targetDir))) { + recommended.add(slug); + } + }), + ); + return recommended; +} export async function promptPluginOptions( descriptors: PluginPromptDescriptor[], diff --git a/packages/create-cli/src/lib/setup/prompts.unit.test.ts b/packages/create-cli/src/lib/setup/prompts.unit.test.ts index 4661a189f..e039a8adf 100644 --- a/packages/create-cli/src/lib/setup/prompts.unit.test.ts +++ b/packages/create-cli/src/lib/setup/prompts.unit.test.ts @@ -1,4 +1,4 @@ -import { promptPluginOptions } from './prompts.js'; +import { promptPluginOptions, promptPluginSelection } from './prompts.js'; import type { PluginPromptDescriptor } from './types.js'; vi.mock('@inquirer/prompts', () => ({ @@ -89,3 +89,143 @@ describe('promptPluginOptions', () => { ).resolves.toStrictEqual({ formats: [] }); }); }); + +describe('promptPluginSelection', () => { + const bindings = [ + { + slug: 'eslint', + title: 'ESLint', + packageName: '@code-pushup/eslint-plugin', + generateConfig: () => ({ imports: [], pluginInit: '' }), + }, + { + slug: 'coverage', + title: 'Code Coverage', + packageName: '@code-pushup/coverage-plugin', + generateConfig: () => ({ imports: [], pluginInit: '' }), + }, + { + slug: 'lighthouse', + title: 'Lighthouse', + packageName: '@code-pushup/lighthouse-plugin', + generateConfig: () => ({ imports: [], pluginInit: '' }), + }, + ]; + + it('should return empty array when given no bindings', async () => { + await expect(promptPluginSelection([], '/test', {})).resolves.toStrictEqual( + [], + ); + + expect(mockCheckbox).not.toHaveBeenCalled(); + }); + + describe('--plugins CLI arg', () => { + it('should return matching bindings for valid slugs', async () => { + await expect( + promptPluginSelection(bindings, '/test', { + plugins: 'eslint,lighthouse', + }), + ).resolves.toStrictEqual([bindings[0], bindings[2]]); + + expect(mockCheckbox).not.toHaveBeenCalled(); + }); + + it('should throw on unknown slug', async () => { + await expect( + promptPluginSelection(bindings, '/test', { plugins: 'eslint,unknown' }), + ).rejects.toThrow('Unknown plugin slugs: unknown'); + }); + }); + + describe('--yes (non-interactive)', () => { + it('should return only recommended plugins when some are recommended', async () => { + const result = await promptPluginSelection( + [ + { ...bindings[0]!, isRecommended: () => Promise.resolve(true) }, + bindings[1]!, + bindings[2]!, + ], + '/test', + { yes: true }, + ); + + expect(result).toBeArrayOfSize(1); + expect(result[0]).toHaveProperty('slug', 'eslint'); + }); + + it('should return all plugins when none are recommended', async () => { + await expect( + promptPluginSelection(bindings, '/test', { yes: true }), + ).resolves.toStrictEqual(bindings); + }); + }); + + describe('interactive prompt', () => { + it('should pre-check recommended plugins and leave others unchecked', async () => { + mockCheckbox.mockResolvedValue(['eslint']); + + await promptPluginSelection( + [ + { ...bindings[0]!, isRecommended: () => Promise.resolve(true) }, + bindings[1]!, + bindings[2]!, + ], + '/test', + {}, + ); + + expect(mockCheckbox).toHaveBeenCalledWith( + expect.objectContaining({ + required: true, + choices: [ + { name: 'ESLint', value: 'eslint', checked: true }, + { name: 'Code Coverage', value: 'coverage', checked: false }, + { name: 'Lighthouse', value: 'lighthouse', checked: false }, + ], + }), + ); + }); + + it('should not pre-check any plugins when none are recommended', async () => { + mockCheckbox.mockResolvedValue(['eslint']); + + await promptPluginSelection(bindings, '/test', {}); + + expect(mockCheckbox).toHaveBeenCalledWith( + expect.objectContaining({ + required: true, + choices: [ + { name: 'ESLint', value: 'eslint', checked: false }, + { name: 'Code Coverage', value: 'coverage', checked: false }, + { name: 'Lighthouse', value: 'lighthouse', checked: false }, + ], + }), + ); + }); + + it('should return only user-selected bindings', async () => { + mockCheckbox.mockResolvedValue(['coverage']); + + await expect( + promptPluginSelection(bindings, '/test', {}), + ).resolves.toStrictEqual([bindings[1]]); + }); + }); + + describe('isRecommended callback', () => { + it('should receive targetDir as argument', async () => { + const isRecommended = vi.fn().mockResolvedValue(false); + + mockCheckbox.mockResolvedValue(['eslint']); + + await promptPluginSelection( + [{ ...bindings[0]!, isRecommended }], + '/my/project', + {}, + ); + + expect(isRecommended).toHaveBeenCalledWith('/my/project'); + }); + }); +}); diff --git a/packages/create-cli/src/lib/setup/types.ts b/packages/create-cli/src/lib/setup/types.ts index d6883eaa2..832935558 100644 --- a/packages/create-cli/src/lib/setup/types.ts +++ b/packages/create-cli/src/lib/setup/types.ts @@ -29,11 +29,19 @@ export type FileSystemAdapter = { ) => Promise; }; +/** + * Defines how a plugin integrates with the setup wizard. + * + * Each supported plugin provides a binding that controls: + * - Pre-selection: `isRecommended` detects if the plugin is relevant for the repository + * - Configuration: `prompts` collect plugin-specific options interactively + * - Code generation: `generateConfig` produces the import and initialization code + */ export type PluginSetupBinding = { slug: PluginMeta['slug']; title: PluginMeta['title']; packageName: NonNullable; - // TODO: #1244 — add async pre-selection callback (e.g. detect eslint.config.js in repo) + isRecommended?: (targetDir: string) => Promise; prompts?: PluginPromptDescriptor[]; generateConfig: ( answers: Record, @@ -50,7 +58,7 @@ export type ImportDeclarationStructure = { export type PluginCodegenResult = { imports: ImportDeclarationStructure[]; pluginInit: string; - // TODO: #1244 — add categories support (categoryRefs for generated categories array) + // TODO: add categories support (categoryRefs for generated categories array) }; type PromptBase = { @@ -86,7 +94,7 @@ export type CliArgs = { 'dry-run'?: boolean; yes?: boolean; 'config-format'?: string; - // TODO: #1244 — add 'plugins' field for CLI-based plugin selection + plugins?: string; 'target-dir'?: string; [key: string]: unknown; }; diff --git a/packages/create-cli/src/lib/setup/wizard.ts b/packages/create-cli/src/lib/setup/wizard.ts index 4ed26ef72..87218aec1 100644 --- a/packages/create-cli/src/lib/setup/wizard.ts +++ b/packages/create-cli/src/lib/setup/wizard.ts @@ -11,7 +11,7 @@ import { resolveConfigFilename, } from './config-format.js'; import { resolveGitignore } from './gitignore.js'; -import { promptPluginOptions } from './prompts.js'; +import { promptPluginOptions, promptPluginSelection } from './prompts.js'; import type { CliArgs, FileChange, @@ -33,13 +33,17 @@ export async function runSetupWizard( const targetDir = cliArgs['target-dir'] ?? process.cwd(); // TODO: #1245 — prompt for standalone vs monorepo mode - // TODO: #1244 — prompt user to select plugins from available bindings + const selectedBindings = await promptPluginSelection( + bindings, + targetDir, + cliArgs, + ); const format = await promptConfigFormat(targetDir, cliArgs); const packageJson = await readPackageJson(targetDir); const filename = resolveConfigFilename(format, packageJson.type === 'module'); - const pluginResults = await asyncSequential(bindings, binding => + const pluginResults = await asyncSequential(selectedBindings, binding => resolveBinding(binding, cliArgs), ); From e830992e1d060702be8b67c824082ef181d50919 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Wed, 4 Mar 2026 08:02:07 -0500 Subject: [PATCH 2/3] fix(create-cli): return no plugins when none recommended --- packages/create-cli/src/lib/setup/codegen.ts | 4 +++- .../create-cli/src/lib/setup/codegen.unit.test.ts | 12 ++++++++---- packages/create-cli/src/lib/setup/prompts.ts | 8 +++----- .../create-cli/src/lib/setup/prompts.unit.test.ts | 4 ++-- packages/create-cli/src/lib/setup/wizard.int.test.ts | 2 ++ .../create-cli/src/lib/setup/wizard.unit.test.ts | 7 +++++-- 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/create-cli/src/lib/setup/codegen.ts b/packages/create-cli/src/lib/setup/codegen.ts index e6337b2d4..3184d1169 100644 --- a/packages/create-cli/src/lib/setup/codegen.ts +++ b/packages/create-cli/src/lib/setup/codegen.ts @@ -68,7 +68,9 @@ function addPlugins( plugins: PluginCodegenResult[], ): void { if (plugins.length === 0) { - builder.addLine('plugins: [],', 1); + builder.addLine('plugins: [', 1); + builder.addLine('// TODO: register some plugins', 2); + builder.addLine('],', 1); } else { builder.addLine('plugins: [', 1); builder.addLines( diff --git a/packages/create-cli/src/lib/setup/codegen.unit.test.ts b/packages/create-cli/src/lib/setup/codegen.unit.test.ts index dd8ed1098..473ece795 100644 --- a/packages/create-cli/src/lib/setup/codegen.unit.test.ts +++ b/packages/create-cli/src/lib/setup/codegen.unit.test.ts @@ -3,12 +3,14 @@ import type { PluginCodegenResult } from './types.js'; describe('generateConfigSource', () => { describe('TypeScript format', () => { - it('should generate config with empty plugins array', () => { + it('should generate config with TODO placeholder when no plugins provided', () => { expect(generateConfigSource([], 'ts')).toMatchInlineSnapshot(` "import type { CoreConfig } from '@code-pushup/models'; export default { - plugins: [], + plugins: [ + // TODO: register some plugins + ], } satisfies CoreConfig; " `); @@ -104,11 +106,13 @@ describe('generateConfigSource', () => { }); describe('JavaScript format', () => { - it('should generate JS config with empty plugins array', () => { + it('should generate JS config with TODO placeholder when no plugins provided', () => { expect(generateConfigSource([], 'js')).toMatchInlineSnapshot(` "/** @type {import('@code-pushup/models').CoreConfig} */ export default { - plugins: [], + plugins: [ + // TODO: register some plugins + ], }; " `); diff --git a/packages/create-cli/src/lib/setup/prompts.ts b/packages/create-cli/src/lib/setup/prompts.ts index 0aff4d81f..96232ddda 100644 --- a/packages/create-cli/src/lib/setup/prompts.ts +++ b/packages/create-cli/src/lib/setup/prompts.ts @@ -10,8 +10,8 @@ import type { * Resolves which plugins to include in the generated config. * * Resolution order (first match wins): - * 1. `--plugins` CLI argument: comma-separated slugs, validated against available bindings - * 2. `--yes` flag: recommended plugins (or all if none recommended) + * 1. `--plugins`: comma-separated slugs, validated against available bindings + * 2. `--yes`: recommended plugins * 3. Interactive: checkbox prompt with recommended plugins pre-checked */ export async function promptPluginSelection( @@ -28,9 +28,7 @@ export async function promptPluginSelection( } const recommended = await detectRecommended(bindings, targetDir); if (cliArgs.yes) { - return recommended.size > 0 - ? bindings.filter(({ slug }) => recommended.has(slug)) - : bindings; + return bindings.filter(({ slug }) => recommended.has(slug)); } const selected = await checkbox({ message: 'Plugins to include:', diff --git a/packages/create-cli/src/lib/setup/prompts.unit.test.ts b/packages/create-cli/src/lib/setup/prompts.unit.test.ts index e039a8adf..772651d1d 100644 --- a/packages/create-cli/src/lib/setup/prompts.unit.test.ts +++ b/packages/create-cli/src/lib/setup/prompts.unit.test.ts @@ -154,10 +154,10 @@ describe('promptPluginSelection', () => { expect(result[0]).toHaveProperty('slug', 'eslint'); }); - it('should return all plugins when none are recommended', async () => { + it('should return no plugins when none are recommended', async () => { await expect( promptPluginSelection(bindings, '/test', { yes: true }), - ).resolves.toStrictEqual(bindings); + ).resolves.toBeArrayOfSize(0); }); }); diff --git a/packages/create-cli/src/lib/setup/wizard.int.test.ts b/packages/create-cli/src/lib/setup/wizard.int.test.ts index 0371cdf92..b434e86e0 100644 --- a/packages/create-cli/src/lib/setup/wizard.int.test.ts +++ b/packages/create-cli/src/lib/setup/wizard.int.test.ts @@ -18,6 +18,7 @@ const TEST_BINDINGS: PluginSetupBinding[] = [ slug: 'alpha', title: 'Alpha Plugin', packageName: '@code-pushup/alpha-plugin', + isRecommended: () => Promise.resolve(true), prompts: [ { key: 'alpha.path', @@ -43,6 +44,7 @@ const TEST_BINDINGS: PluginSetupBinding[] = [ slug: 'beta', title: 'Beta Plugin', packageName: '@code-pushup/beta-plugin', + isRecommended: () => Promise.resolve(true), generateConfig: () => ({ imports: [ { diff --git a/packages/create-cli/src/lib/setup/wizard.unit.test.ts b/packages/create-cli/src/lib/setup/wizard.unit.test.ts index 965e3244f..6a03126b5 100644 --- a/packages/create-cli/src/lib/setup/wizard.unit.test.ts +++ b/packages/create-cli/src/lib/setup/wizard.unit.test.ts @@ -15,6 +15,7 @@ const TEST_BINDING: PluginSetupBinding = { slug: 'test-plugin', title: 'Test Plugin', packageName: '@code-pushup/test-plugin', + isRecommended: () => Promise.resolve(true), generateConfig: () => ({ imports: [ { @@ -72,7 +73,7 @@ describe('runSetupWizard', () => { expect(logger.info).toHaveBeenCalledWith('Dry run — no files written.'); }); - it('should generate empty config with no bindings', async () => { + it('should generate config with TODO placeholder when no bindings provided', async () => { await runSetupWizard([], { yes: true, 'target-dir': MEMFS_VOLUME, @@ -83,7 +84,9 @@ describe('runSetupWizard', () => { "import type { CoreConfig } from '@code-pushup/models'; export default { - plugins: [], + plugins: [ + // TODO: register some plugins + ], } satisfies CoreConfig; " `); From 2507d652b765db6973d5353ada8963d609310d80 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Wed, 4 Mar 2026 09:01:30 -0500 Subject: [PATCH 3/3] refactor(create-cli): extract plugin slug utils --- packages/create-cli/src/index.ts | 17 +++++-- packages/create-cli/src/lib/setup/plugins.ts | 29 ++++++++++++ .../src/lib/setup/plugins.unit.test.ts | 45 +++++++++++++++++++ packages/create-cli/src/lib/setup/prompts.ts | 38 +++------------- .../src/lib/setup/prompts.unit.test.ts | 8 +--- packages/create-cli/src/lib/setup/types.ts | 2 +- 6 files changed, 95 insertions(+), 44 deletions(-) create mode 100644 packages/create-cli/src/lib/setup/plugins.ts create mode 100644 packages/create-cli/src/lib/setup/plugins.unit.test.ts diff --git a/packages/create-cli/src/index.ts b/packages/create-cli/src/index.ts index 9908149bd..34105564e 100755 --- a/packages/create-cli/src/index.ts +++ b/packages/create-cli/src/index.ts @@ -1,9 +1,16 @@ #! /usr/bin/env node import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { CONFIG_FILE_FORMATS } from './lib/setup/types.js'; +import { parsePluginSlugs, validatePluginSlugs } from './lib/setup/plugins.js'; +import { + CONFIG_FILE_FORMATS, + type PluginSetupBinding, +} from './lib/setup/types.js'; import { runSetupWizard } from './lib/setup/wizard.js'; +// TODO: create, import and pass plugin bindings (eslint, coverage, lighthouse, typescript, js-packages, jsdocs, axe) +const bindings: PluginSetupBinding[] = []; + const argv = await yargs(hideBin(process.argv)) .option('dry-run', { type: 'boolean', @@ -24,8 +31,12 @@ const argv = await yargs(hideBin(process.argv)) .option('plugins', { type: 'string', describe: 'Comma-separated plugin slugs to include (e.g. eslint,coverage)', + coerce: parsePluginSlugs, + }) + .check(parsed => { + validatePluginSlugs(bindings, parsed.plugins); + return true; }) .parse(); -// TODO: create, import and pass plugin bindings (eslint, coverage, lighthouse, typescript, js-packages, jsdocs, axe) -await runSetupWizard([], argv); +await runSetupWizard(bindings, argv); diff --git a/packages/create-cli/src/lib/setup/plugins.ts b/packages/create-cli/src/lib/setup/plugins.ts new file mode 100644 index 000000000..60f637a50 --- /dev/null +++ b/packages/create-cli/src/lib/setup/plugins.ts @@ -0,0 +1,29 @@ +import type { PluginSetupBinding } from './types.js'; + +/** Parses a comma-separated string of plugin slugs into a deduplicated array. */ +export function parsePluginSlugs(value: string): string[] { + return [ + ...new Set( + value + .split(',') + .map(s => s.trim()) + .filter(Boolean), + ), + ]; +} + +/** Throws if any slug is not found in the available bindings. */ +export function validatePluginSlugs( + bindings: PluginSetupBinding[], + plugins?: string[], +): void { + if (plugins == null || plugins.length === 0) { + return; + } + const unknown = plugins.filter(slug => !bindings.some(b => b.slug === slug)); + if (unknown.length > 0) { + throw new TypeError( + `Unknown plugin slugs: ${unknown.join(', ')}. Available: ${bindings.map(b => b.slug).join(', ')}`, + ); + } +} diff --git a/packages/create-cli/src/lib/setup/plugins.unit.test.ts b/packages/create-cli/src/lib/setup/plugins.unit.test.ts new file mode 100644 index 000000000..d9e75ffdc --- /dev/null +++ b/packages/create-cli/src/lib/setup/plugins.unit.test.ts @@ -0,0 +1,45 @@ +import { parsePluginSlugs, validatePluginSlugs } from './plugins.js'; + +describe('parsePluginSlugs', () => { + it.each([ + ['eslint,coverage', ['eslint', 'coverage']], + [' eslint , coverage ', ['eslint', 'coverage']], + ['eslint,eslint', ['eslint']], + ['eslint,,coverage', ['eslint', 'coverage']], + ])('should parse %j into %j', (input, expected) => { + expect(parsePluginSlugs(input)).toStrictEqual(expected); + }); +}); + +describe('validatePluginSlugs', () => { + const bindings = [ + { + slug: 'eslint', + title: 'ESLint', + packageName: '@code-pushup/eslint-plugin', + generateConfig: () => ({ imports: [], pluginInit: '' }), + }, + { + slug: 'coverage', + title: 'Code Coverage', + packageName: '@code-pushup/coverage-plugin', + generateConfig: () => ({ imports: [], pluginInit: '' }), + }, + ]; + + it('should not throw for valid or missing slugs', () => { + expect(() => validatePluginSlugs(bindings)).not.toThrow(); + expect(() => + validatePluginSlugs(bindings, ['eslint', 'coverage']), + ).not.toThrow(); + }); + + it('should throw TypeError on unknown slug', () => { + expect(() => validatePluginSlugs(bindings, ['eslint', 'unknown'])).toThrow( + TypeError, + ); + expect(() => validatePluginSlugs(bindings, ['eslint', 'unknown'])).toThrow( + 'Unknown plugin slugs: unknown', + ); + }); +}); diff --git a/packages/create-cli/src/lib/setup/prompts.ts b/packages/create-cli/src/lib/setup/prompts.ts index 96232ddda..7811ada06 100644 --- a/packages/create-cli/src/lib/setup/prompts.ts +++ b/packages/create-cli/src/lib/setup/prompts.ts @@ -10,24 +10,23 @@ import type { * Resolves which plugins to include in the generated config. * * Resolution order (first match wins): - * 1. `--plugins`: comma-separated slugs, validated against available bindings + * 1. `--plugins`: user-provided slugs * 2. `--yes`: recommended plugins * 3. Interactive: checkbox prompt with recommended plugins pre-checked */ export async function promptPluginSelection( bindings: PluginSetupBinding[], targetDir: string, - cliArgs: CliArgs, + { plugins, yes }: CliArgs, ): Promise { if (bindings.length === 0) { return []; } - const slugs = parsePluginSlugs(cliArgs.plugins); - if (slugs != null) { - return filterBindingsBySlugs(bindings, slugs); + if (plugins != null && plugins.length > 0) { + return bindings.filter(b => plugins.includes(b.slug)); } const recommended = await detectRecommended(bindings, targetDir); - if (cliArgs.yes) { + if (yes) { return bindings.filter(({ slug }) => recommended.has(slug)); } const selected = await checkbox({ @@ -43,33 +42,6 @@ export async function promptPluginSelection( return bindings.filter(({ slug }) => selectedSet.has(slug)); } -function parsePluginSlugs(value: string | undefined): string[] | null { - if (value == null || value.trim() === '') { - return null; - } - return [ - ...new Set( - value - .split(',') - .map(s => s.trim()) - .filter(Boolean), - ), - ]; -} - -function filterBindingsBySlugs( - bindings: PluginSetupBinding[], - slugs: string[], -): PluginSetupBinding[] { - const unknown = slugs.filter(slug => !bindings.some(b => b.slug === slug)); - if (unknown.length > 0) { - throw new Error( - `Unknown plugin slugs: ${unknown.join(', ')}. Available: ${bindings.map(b => b.slug).join(', ')}`, - ); - } - return bindings.filter(b => slugs.includes(b.slug)); -} - /** * Calls each binding's `isRecommended` callback (if provided) * and collects the slugs of bindings that returned `true`. diff --git a/packages/create-cli/src/lib/setup/prompts.unit.test.ts b/packages/create-cli/src/lib/setup/prompts.unit.test.ts index 772651d1d..ee309546d 100644 --- a/packages/create-cli/src/lib/setup/prompts.unit.test.ts +++ b/packages/create-cli/src/lib/setup/prompts.unit.test.ts @@ -124,18 +124,12 @@ describe('promptPluginSelection', () => { it('should return matching bindings for valid slugs', async () => { await expect( promptPluginSelection(bindings, '/test', { - plugins: 'eslint,lighthouse', + plugins: ['eslint', 'lighthouse'], }), ).resolves.toStrictEqual([bindings[0], bindings[2]]); expect(mockCheckbox).not.toHaveBeenCalled(); }); - - it('should throw on unknown slug', async () => { - await expect( - promptPluginSelection(bindings, '/test', { plugins: 'eslint,unknown' }), - ).rejects.toThrow('Unknown plugin slugs: unknown'); - }); }); describe('--yes (non-interactive)', () => { diff --git a/packages/create-cli/src/lib/setup/types.ts b/packages/create-cli/src/lib/setup/types.ts index 832935558..7bdc9956e 100644 --- a/packages/create-cli/src/lib/setup/types.ts +++ b/packages/create-cli/src/lib/setup/types.ts @@ -94,7 +94,7 @@ export type CliArgs = { 'dry-run'?: boolean; yes?: boolean; 'config-format'?: string; - plugins?: string; + plugins?: string[]; 'target-dir'?: string; [key: string]: unknown; };