Skip to content

Commit f24ae51

Browse files
committed
refactor(create-cli): extract plugin slug utils
1 parent 28395ba commit f24ae51

File tree

6 files changed

+95
-44
lines changed

6 files changed

+95
-44
lines changed

packages/create-cli/src/index.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
#! /usr/bin/env node
22
import yargs from 'yargs';
33
import { hideBin } from 'yargs/helpers';
4-
import { CONFIG_FILE_FORMATS } from './lib/setup/types.js';
4+
import { parsePluginSlugs, validatePluginSlugs } from './lib/setup/plugins.js';
5+
import {
6+
CONFIG_FILE_FORMATS,
7+
type PluginSetupBinding,
8+
} from './lib/setup/types.js';
59
import { runSetupWizard } from './lib/setup/wizard.js';
610

11+
// TODO: create, import and pass plugin bindings (eslint, coverage, lighthouse, typescript, js-packages, jsdocs, axe)
12+
const bindings: PluginSetupBinding[] = [];
13+
714
const argv = await yargs(hideBin(process.argv))
815
.option('dry-run', {
916
type: 'boolean',
@@ -24,8 +31,12 @@ const argv = await yargs(hideBin(process.argv))
2431
.option('plugins', {
2532
type: 'string',
2633
describe: 'Comma-separated plugin slugs to include (e.g. eslint,coverage)',
34+
coerce: parsePluginSlugs,
35+
})
36+
.check(parsed => {
37+
validatePluginSlugs(bindings, parsed.plugins);
38+
return true;
2739
})
2840
.parse();
2941

30-
// TODO: create, import and pass plugin bindings (eslint, coverage, lighthouse, typescript, js-packages, jsdocs, axe)
31-
await runSetupWizard([], argv);
42+
await runSetupWizard(bindings, argv);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { PluginSetupBinding } from './types.js';
2+
3+
/** Parses a comma-separated string of plugin slugs into a deduplicated array. */
4+
export function parsePluginSlugs(value: string): string[] {
5+
return [
6+
...new Set(
7+
value
8+
.split(',')
9+
.map(s => s.trim())
10+
.filter(Boolean),
11+
),
12+
];
13+
}
14+
15+
/** Throws if any slug is not found in the available bindings. */
16+
export function validatePluginSlugs(
17+
bindings: PluginSetupBinding[],
18+
plugins?: string[],
19+
): void {
20+
if (plugins == null || plugins.length === 0) {
21+
return;
22+
}
23+
const unknown = plugins.filter(slug => !bindings.some(b => b.slug === slug));
24+
if (unknown.length > 0) {
25+
throw new TypeError(
26+
`Unknown plugin slugs: ${unknown.join(', ')}. Available: ${bindings.map(b => b.slug).join(', ')}`,
27+
);
28+
}
29+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { parsePluginSlugs, validatePluginSlugs } from './plugins.js';
2+
3+
describe('parsePluginSlugs', () => {
4+
it.each([
5+
['eslint,coverage', ['eslint', 'coverage']],
6+
[' eslint , coverage ', ['eslint', 'coverage']],
7+
['eslint,eslint', ['eslint']],
8+
['eslint,,coverage', ['eslint', 'coverage']],
9+
])('should parse %j into %j', (input, expected) => {
10+
expect(parsePluginSlugs(input)).toStrictEqual(expected);
11+
});
12+
});
13+
14+
describe('validatePluginSlugs', () => {
15+
const bindings = [
16+
{
17+
slug: 'eslint',
18+
title: 'ESLint',
19+
packageName: '@code-pushup/eslint-plugin',
20+
generateConfig: () => ({ imports: [], pluginInit: '' }),
21+
},
22+
{
23+
slug: 'coverage',
24+
title: 'Code Coverage',
25+
packageName: '@code-pushup/coverage-plugin',
26+
generateConfig: () => ({ imports: [], pluginInit: '' }),
27+
},
28+
];
29+
30+
it('should not throw for valid or missing slugs', () => {
31+
expect(() => validatePluginSlugs(bindings)).not.toThrow();
32+
expect(() =>
33+
validatePluginSlugs(bindings, ['eslint', 'coverage']),
34+
).not.toThrow();
35+
});
36+
37+
it('should throw TypeError on unknown slug', () => {
38+
expect(() => validatePluginSlugs(bindings, ['eslint', 'unknown'])).toThrow(
39+
TypeError,
40+
);
41+
expect(() => validatePluginSlugs(bindings, ['eslint', 'unknown'])).toThrow(
42+
'Unknown plugin slugs: unknown',
43+
);
44+
});
45+
});

packages/create-cli/src/lib/setup/prompts.ts

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,23 @@ import type {
1010
* Resolves which plugins to include in the generated config.
1111
*
1212
* Resolution order (first match wins):
13-
* 1. `--plugins`: comma-separated slugs, validated against available bindings
13+
* 1. `--plugins`: user-provided slugs
1414
* 2. `--yes`: recommended plugins
1515
* 3. Interactive: checkbox prompt with recommended plugins pre-checked
1616
*/
1717
export async function promptPluginSelection(
1818
bindings: PluginSetupBinding[],
1919
targetDir: string,
20-
cliArgs: CliArgs,
20+
{ plugins, yes }: CliArgs,
2121
): Promise<PluginSetupBinding[]> {
2222
if (bindings.length === 0) {
2323
return [];
2424
}
25-
const slugs = parsePluginSlugs(cliArgs.plugins);
26-
if (slugs != null) {
27-
return filterBindingsBySlugs(bindings, slugs);
25+
if (plugins != null && plugins.length > 0) {
26+
return bindings.filter(b => plugins.includes(b.slug));
2827
}
2928
const recommended = await detectRecommended(bindings, targetDir);
30-
if (cliArgs.yes) {
29+
if (yes) {
3130
return bindings.filter(({ slug }) => recommended.has(slug));
3231
}
3332
const selected = await checkbox({
@@ -43,33 +42,6 @@ export async function promptPluginSelection(
4342
return bindings.filter(({ slug }) => selectedSet.has(slug));
4443
}
4544

46-
function parsePluginSlugs(value: string | undefined): string[] | null {
47-
if (value == null || value.trim() === '') {
48-
return null;
49-
}
50-
return [
51-
...new Set(
52-
value
53-
.split(',')
54-
.map(s => s.trim())
55-
.filter(Boolean),
56-
),
57-
];
58-
}
59-
60-
function filterBindingsBySlugs(
61-
bindings: PluginSetupBinding[],
62-
slugs: string[],
63-
): PluginSetupBinding[] {
64-
const unknown = slugs.filter(slug => !bindings.some(b => b.slug === slug));
65-
if (unknown.length > 0) {
66-
throw new Error(
67-
`Unknown plugin slugs: ${unknown.join(', ')}. Available: ${bindings.map(b => b.slug).join(', ')}`,
68-
);
69-
}
70-
return bindings.filter(b => slugs.includes(b.slug));
71-
}
72-
7345
/**
7446
* Calls each binding's `isRecommended` callback (if provided)
7547
* and collects the slugs of bindings that returned `true`.

packages/create-cli/src/lib/setup/prompts.unit.test.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,18 +124,12 @@ describe('promptPluginSelection', () => {
124124
it('should return matching bindings for valid slugs', async () => {
125125
await expect(
126126
promptPluginSelection(bindings, '/test', {
127-
plugins: 'eslint,lighthouse',
127+
plugins: ['eslint', 'lighthouse'],
128128
}),
129129
).resolves.toStrictEqual([bindings[0], bindings[2]]);
130130

131131
expect(mockCheckbox).not.toHaveBeenCalled();
132132
});
133-
134-
it('should throw on unknown slug', async () => {
135-
await expect(
136-
promptPluginSelection(bindings, '/test', { plugins: 'eslint,unknown' }),
137-
).rejects.toThrow('Unknown plugin slugs: unknown');
138-
});
139133
});
140134

141135
describe('--yes (non-interactive)', () => {

packages/create-cli/src/lib/setup/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export type CliArgs = {
9494
'dry-run'?: boolean;
9595
yes?: boolean;
9696
'config-format'?: string;
97-
plugins?: string;
97+
plugins?: string[];
9898
'target-dir'?: string;
9999
[key: string]: unknown;
100100
};

0 commit comments

Comments
 (0)