Skip to content

Commit 5eb3416

Browse files
authored
refactor: extract wizard infrastructure into utils (#1283)
1 parent 47eb97a commit 5eb3416

33 files changed

Lines changed: 706 additions & 366 deletions

File tree

packages/create-cli/src/index.ts

Lines changed: 3 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#! /usr/bin/env node
2-
import yargs from 'yargs';
32
import { hideBin } from 'yargs/helpers';
43
import { axeSetupBinding } from '@code-pushup/axe-plugin';
54
import { coverageSetupBinding } from '@code-pushup/coverage-plugin';
@@ -8,13 +7,8 @@ import { jsPackagesSetupBinding } from '@code-pushup/js-packages-plugin';
87
import { jsDocsSetupBinding } from '@code-pushup/jsdocs-plugin';
98
import { lighthouseSetupBinding } from '@code-pushup/lighthouse-plugin';
109
import { typescriptSetupBinding } from '@code-pushup/typescript-plugin';
11-
import { parsePluginSlugs, validatePluginSlugs } from './lib/setup/plugins.js';
12-
import {
13-
CI_PROVIDERS,
14-
CONFIG_FILE_FORMATS,
15-
type PluginSetupBinding,
16-
SETUP_MODES,
17-
} from './lib/setup/types.js';
10+
import { yargsCli } from './lib/setup/cli-args.js';
11+
import type { PluginSetupBinding } from './lib/setup/types.js';
1812
import { runSetupWizard } from './lib/setup/wizard.js';
1913

2014
const bindings: PluginSetupBinding[] = [
@@ -27,42 +21,6 @@ const bindings: PluginSetupBinding[] = [
2721
jsDocsSetupBinding,
2822
];
2923

30-
const argv = await yargs(hideBin(process.argv))
31-
.option('dry-run', {
32-
type: 'boolean',
33-
default: false,
34-
describe: 'Preview changes without writing files',
35-
})
36-
.option('yes', {
37-
alias: 'y',
38-
type: 'boolean',
39-
default: false,
40-
describe: 'Skip prompts and use defaults',
41-
})
42-
.option('config-format', {
43-
type: 'string',
44-
choices: CONFIG_FILE_FORMATS,
45-
describe: 'Config file format (default: auto-detected from project)',
46-
})
47-
.option('plugins', {
48-
type: 'string',
49-
describe: 'Comma-separated plugin slugs to include (e.g. eslint,coverage)',
50-
coerce: parsePluginSlugs,
51-
})
52-
.option('mode', {
53-
type: 'string',
54-
choices: SETUP_MODES,
55-
describe: 'Setup mode (default: auto-detected from project)',
56-
})
57-
.option('ci', {
58-
type: 'string',
59-
choices: CI_PROVIDERS,
60-
describe: 'CI/CD integration (github, gitlab, or none)',
61-
})
62-
.check(parsed => {
63-
validatePluginSlugs(bindings, parsed.plugins);
64-
return true;
65-
})
66-
.parse();
24+
const argv = await yargsCli(bindings).parse(hideBin(process.argv));
6725

6826
await runSetupWizard(bindings, argv);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { select } from '@inquirer/prompts';
22
import { vol } from 'memfs';
33
import { MEMFS_VOLUME } from '@code-pushup/test-utils';
4+
import { createTree } from '@code-pushup/utils';
45
import { promptCiProvider, resolveCi } from './ci.js';
56
import type { ConfigContext } from './types.js';
6-
import { createTree } from './virtual-fs.js';
77

88
vi.mock('@inquirer/prompts', () => ({
99
select: vi.fn(),
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import yargs, { type Argv } from 'yargs';
2+
import { parsePluginSlugs, validatePluginSlugs } from './plugins.js';
3+
import {
4+
CI_PROVIDERS,
5+
CONFIG_FILE_FORMATS,
6+
type PluginSetupBinding,
7+
SETUP_MODES,
8+
} from './types.js';
9+
10+
export function yargsCli(bindings: PluginSetupBinding[]): Argv {
11+
return yargs()
12+
.scriptName('create-cli')
13+
.usage('$0 [options]')
14+
.parserConfiguration({ 'dot-notation': false })
15+
.option('dry-run', {
16+
type: 'boolean',
17+
default: false,
18+
describe: 'Preview changes without writing files',
19+
})
20+
.option('yes', {
21+
alias: 'y',
22+
type: 'boolean',
23+
default: false,
24+
describe: 'Skip prompts and use defaults',
25+
})
26+
.option('config-format', {
27+
type: 'string',
28+
choices: CONFIG_FILE_FORMATS,
29+
describe: 'Config file format (default: auto-detected from project)',
30+
})
31+
.option('plugins', {
32+
type: 'string',
33+
describe:
34+
'Comma-separated plugin slugs to include (e.g. eslint,coverage)',
35+
coerce: parsePluginSlugs,
36+
})
37+
.option('mode', {
38+
type: 'string',
39+
choices: SETUP_MODES,
40+
describe: 'Setup mode (default: auto-detected from project)',
41+
})
42+
.option('ci', {
43+
type: 'string',
44+
choices: CI_PROVIDERS,
45+
describe: 'CI/CD integration (github, gitlab, or none)',
46+
})
47+
.check(parsed => {
48+
validatePluginSlugs(bindings, parsed.plugins);
49+
return true;
50+
})
51+
.help()
52+
.version();
53+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { yargsCli } from './cli-args.js';
2+
import type { PluginSetupBinding } from './types.js';
3+
4+
const bareBindings: PluginSetupBinding[] = [
5+
{
6+
slug: 'eslint',
7+
title: 'ESLint',
8+
packageName: '@code-pushup/eslint-plugin',
9+
generateConfig: () => ({ imports: [], pluginInit: [] }),
10+
},
11+
];
12+
13+
describe('yargsCli', () => {
14+
it('should expose --eslint.patterns as a flat key', async () => {
15+
const argv = await yargsCli(bareBindings).parse([
16+
'--eslint.patterns',
17+
'src',
18+
]);
19+
20+
expect(argv['eslint.patterns']).toBe('src');
21+
});
22+
23+
it('should expose --no-eslint.categories as a flat false', async () => {
24+
const argv = await yargsCli(bareBindings).parse(['--no-eslint.categories']);
25+
26+
expect(argv['eslint.categories']).toBeFalse();
27+
});
28+
29+
it('should expose --eslint.categories without a value as a flat true', async () => {
30+
const argv = await yargsCli(bareBindings).parse(['--eslint.categories']);
31+
32+
expect(argv['eslint.categories']).toBeTrue();
33+
});
34+
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { vol } from 'memfs';
22
import { readFile } from 'node:fs/promises';
33
import { MEMFS_VOLUME } from '@code-pushup/test-utils';
4+
import { createTree } from '@code-pushup/utils';
45
import { resolveGitignore } from './gitignore.js';
5-
import { createTree } from './virtual-fs.js';
66

77
describe('resolveGitignore', () => {
88
it('should create .gitignore with comment when it does not exist', async () => {

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { select } from '@inquirer/prompts';
22
import { vol } from 'memfs';
33
import { MEMFS_VOLUME } from '@code-pushup/test-utils';
4-
import { logger } from '@code-pushup/utils';
4+
import { createTree, logger } from '@code-pushup/utils';
55
import { addCodePushUpCommand, promptSetupMode } from './monorepo.js';
66
import type { WizardProject } from './types.js';
7-
import { createTree } from './virtual-fs.js';
87

98
vi.mock('@inquirer/prompts', () => ({
109
select: vi.fn(),

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

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import type { PluginCodegenResult } from '@code-pushup/models';
2-
import type { MonorepoTool } from '@code-pushup/utils';
2+
import type { MonorepoTool, Tree } from '@code-pushup/utils';
33

44
export type {
55
CategoryCodegenConfig,
66
ImportDeclarationStructure,
77
PluginAnswer,
8+
PluginCodegenInput,
89
PluginCodegenResult,
910
PluginPromptDescriptor,
1011
PluginSetupBinding,
1112
PluginSetupTree,
1213
} from '@code-pushup/models';
1314

15+
export type { FileChange, FileSystemAdapter, Tree } from '@code-pushup/utils';
16+
1417
export const CI_PROVIDERS = ['github', 'gitlab', 'none'] as const;
1518
export type CiProvider = (typeof CI_PROVIDERS)[number];
1619

@@ -58,31 +61,3 @@ export type WriteContext = {
5861
configFilename: string;
5962
isEsm: boolean;
6063
};
61-
62-
/** A single file operation recorded by the virtual tree. */
63-
export type FileChange = {
64-
path: string;
65-
type: 'CREATE' | 'UPDATE';
66-
content: string;
67-
};
68-
69-
/** Virtual file system that buffers writes in memory until flushed to disk. */
70-
export type Tree = {
71-
root: string;
72-
exists: (filePath: string) => Promise<boolean>;
73-
read: (filePath: string) => Promise<string | null>;
74-
write: (filePath: string, content: string) => Promise<void>;
75-
listChanges: () => FileChange[];
76-
flush: () => Promise<void>;
77-
};
78-
79-
/** Abstraction over `node:fs` used by the virtual tree for disk I/O. */
80-
export type FileSystemAdapter = {
81-
readFile: (path: string, encoding: 'utf8') => Promise<string>;
82-
writeFile: (path: string, content: string) => Promise<void>;
83-
exists: (path: string) => Promise<boolean>;
84-
mkdir: (
85-
path: string,
86-
options: { recursive: true },
87-
) => Promise<string | undefined>;
88-
};

packages/create-cli/src/lib/setup/virtual-fs.ts

Lines changed: 0 additions & 67 deletions
This file was deleted.

packages/create-cli/src/lib/setup/wizard.int.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const TEST_BINDINGS: PluginSetupBinding[] = [
2828
default: 'alpha.config.js',
2929
},
3030
],
31-
generateConfig(answers) {
31+
generateConfig({ answers }) {
3232
const configPath = answers['alpha.path'] ?? 'alpha.config.js';
3333
return {
3434
imports: [

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import path from 'node:path';
33
import {
44
type MonorepoTool,
55
asyncSequential,
6+
createTree,
67
formatAsciiTable,
78
getGitRoot,
89
logger,
@@ -35,7 +36,6 @@ import type {
3536
Tree,
3637
WriteContext,
3738
} from './types.js';
38-
import { createTree } from './virtual-fs.js';
3939

4040
/**
4141
* Runs the interactive setup wizard that generates a Code PushUp config file.
@@ -93,7 +93,12 @@ async function resolveBinding(
9393
tree: Pick<Tree, 'read' | 'write'>,
9494
): Promise<PluginCodegenResult> {
9595
if (!binding.prompts) {
96-
return binding.generateConfig({}, tree);
96+
return binding.generateConfig({
97+
tree,
98+
targetDir,
99+
cliArgs,
100+
answers: {},
101+
});
97102
}
98103
logger.newline();
99104
logger.info(ansis.bold(binding.title));
@@ -102,7 +107,7 @@ async function resolveBinding(
102107
descriptors.length > 0
103108
? await promptPluginOptions(descriptors, cliArgs)
104109
: {};
105-
return binding.generateConfig(answers, tree);
110+
return binding.generateConfig({ tree, targetDir, cliArgs, answers });
106111
}
107112

108113
async function writeStandaloneConfig(

0 commit comments

Comments
 (0)