Skip to content

Commit b279c73

Browse files
committed
refactor: standardize binding answer parsing
1 parent 89b6f1c commit b279c73

5 files changed

Lines changed: 102 additions & 75 deletions

File tree

packages/create-cli/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ Each plugin exposes its own configuration keys that can be passed as CLI argumen
3131

3232
#### ESLint
3333

34-
| Option | Type | Default | Description |
35-
| ------------------------- | --------- | ------------- | -------------------------- |
36-
| **`--eslint.eslintrc`** | `string` | auto-detected | Path to ESLint config |
37-
| **`--eslint.patterns`** | `string` | `src` or `.` | File patterns to lint |
38-
| **`--eslint.categories`** | `boolean` | `true` | Add recommended categories |
34+
| Option | Type | Default | Description |
35+
| ------------------------- | --------- | ------------- | --------------------- |
36+
| **`--eslint.eslintrc`** | `string` | auto-detected | Path to ESLint config |
37+
| **`--eslint.patterns`** | `string` | `src` or `.` | File patterns to lint |
38+
| **`--eslint.categories`** | `boolean` | `true` | Add ESLint categories |
3939

4040
#### Coverage
4141

@@ -47,7 +47,7 @@ Each plugin exposes its own configuration keys that can be passed as CLI argumen
4747
| **`--coverage.testCommand`** | `string` | auto-detected | Command to run tests |
4848
| **`--coverage.types`** | `('function'` \| `'branch'` \| `'line')[]` | all | Coverage types to measure |
4949
| **`--coverage.continueOnFail`** | `boolean` | `true` | Continue if test command fails |
50-
| **`--coverage.categories`** | `boolean` | `true` | Add code coverage category |
50+
| **`--coverage.categories`** | `boolean` | `true` | Add Code coverage categories |
5151

5252
### Examples
5353

packages/plugin-coverage/src/lib/binding.ts

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import type {
88
PluginSetupTree,
99
} from '@code-pushup/models';
1010
import {
11+
answerArray,
12+
answerBoolean,
13+
answerString,
1114
hasDependency,
1215
pluralize,
1316
readJsonFile,
@@ -119,7 +122,7 @@ export const coverageSetupBinding = {
119122
},
120123
{
121124
key: 'coverage.categories',
122-
message: 'Add code coverage category?',
125+
message: 'Add Code coverage categories?',
123126
type: 'confirm',
124127
default: true,
125128
},
@@ -129,37 +132,28 @@ export const coverageSetupBinding = {
129132
answers: Record<string, PluginAnswer>,
130133
tree?: PluginSetupTree,
131134
) => {
132-
const args = parseAnswers(answers);
133-
const lcovConfigured = await configureLcovReporter(args, tree);
135+
const options = parseAnswers(answers);
136+
const lcovConfigured = await configureLcovReporter(options, tree);
134137
return {
135138
imports: [
136139
{ moduleSpecifier: PACKAGE_NAME, defaultImport: 'coveragePlugin' },
137140
],
138-
pluginInit: formatPluginInit(args, lcovConfigured),
139-
...(args.categories ? { categories: CATEGORIES } : {}),
141+
pluginInit: formatPluginInit(options, lcovConfigured),
142+
...(options.categories ? { categories: CATEGORIES } : {}),
140143
};
141144
},
142145
} satisfies PluginSetupBinding;
143146

144147
function parseAnswers(answers: Record<string, PluginAnswer>): CoverageOptions {
145-
const string = (key: string) => {
146-
const value = answers[key];
147-
return typeof value === 'string' ? value : '';
148-
};
149-
const types = answers['coverage.types'];
150148
return {
151-
framework: string('coverage.framework'),
152-
configFile: string('coverage.configFile'),
153-
reportPath: string('coverage.reportPath') || DEFAULT_REPORT_PATH,
154-
testCommand: string('coverage.testCommand'),
155-
types: Array.isArray(types)
156-
? types
157-
: (typeof types === 'string' ? types : '')
158-
.split(',')
159-
.map(item => item.trim())
160-
.filter(Boolean),
161-
continueOnFail: answers['coverage.continueOnFail'] !== false,
162-
categories: answers['coverage.categories'] !== false,
149+
framework: answerString(answers, 'coverage.framework'),
150+
configFile: answerString(answers, 'coverage.configFile'),
151+
reportPath:
152+
answerString(answers, 'coverage.reportPath') || DEFAULT_REPORT_PATH,
153+
testCommand: answerString(answers, 'coverage.testCommand'),
154+
types: answerArray(answers, 'coverage.types'),
155+
continueOnFail: answerBoolean(answers, 'coverage.continueOnFail'),
156+
categories: answerBoolean(answers, 'coverage.categories'),
163157
};
164158
}
165159

packages/plugin-eslint/src/lib/binding.ts

Lines changed: 42 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import type {
77
PluginSetupBinding,
88
} from '@code-pushup/models';
99
import {
10+
answerArray,
11+
answerBoolean,
12+
answerString,
1013
directoryExists,
1114
hasDependency,
1215
readJsonFile,
@@ -54,6 +57,12 @@ const ESLINT_CATEGORIES: CategoryConfig[] = [
5457
},
5558
];
5659

60+
type EslintOptions = {
61+
eslintrc: string;
62+
patterns: string[];
63+
categories: boolean;
64+
};
65+
5766
export const eslintSetupBinding = {
5867
slug: ESLINT_PLUGIN_SLUG,
5968
title: ESLINT_PLUGIN_TITLE,
@@ -76,36 +85,47 @@ export const eslintSetupBinding = {
7685
},
7786
{
7887
key: 'eslint.categories',
79-
message: 'Add recommended categories (bug prevention, code style)?',
88+
message: 'Add ESLint categories?',
8089
type: 'confirm',
8190
default: true,
8291
},
8392
],
8493
generateConfig: (answers: Record<string, PluginAnswer>) => {
85-
const withCategories = answers['eslint.categories'] !== false;
86-
const args = [
87-
resolveEslintrc(answers['eslint.eslintrc']),
88-
resolvePatterns(answers['eslint.patterns']),
89-
].filter(Boolean);
90-
94+
const options = parseAnswers(answers);
9195
return {
9296
imports: [
9397
{ moduleSpecifier: PACKAGE_NAME, defaultImport: 'eslintPlugin' },
9498
],
95-
pluginInit:
96-
args.length > 0
97-
? `await eslintPlugin({ ${args.join(', ')} })`
98-
: 'await eslintPlugin()',
99-
...(withCategories ? { categories: ESLINT_CATEGORIES } : {}),
99+
pluginInit: formatPluginInit(options),
100+
...(options.categories ? { categories: ESLINT_CATEGORIES } : {}),
100101
};
101102
},
102103
} satisfies PluginSetupBinding;
103104

104-
async function detectEslintConfig(
105-
targetDir: string,
106-
): Promise<string | undefined> {
107-
const files = await readdir(targetDir, { encoding: 'utf8' });
108-
return files.find(file => ESLINT_CONFIG_PATTERN.test(file));
105+
function parseAnswers(answers: Record<string, PluginAnswer>): EslintOptions {
106+
return {
107+
eslintrc: answerString(answers, 'eslint.eslintrc'),
108+
patterns: answerArray(answers, 'eslint.patterns'),
109+
categories: answerBoolean(answers, 'eslint.categories'),
110+
};
111+
}
112+
113+
function formatPluginInit({ eslintrc, patterns }: EslintOptions): string {
114+
const useCustomEslintrc =
115+
eslintrc !== '' && !ESLINT_CONFIG_PATTERN.test(eslintrc);
116+
const customPatterns = patterns
117+
.filter(s => s !== '' && s !== DEFAULT_PATTERN)
118+
.map(singleQuote);
119+
120+
const body = [
121+
useCustomEslintrc ? `eslintrc: ${singleQuote(eslintrc)}` : '',
122+
customPatterns.length === 1 ? `patterns: ${customPatterns[0]}` : '',
123+
customPatterns.length > 1 ? `patterns: [${customPatterns.join(', ')}]` : '',
124+
]
125+
.filter(Boolean)
126+
.join(', ');
127+
128+
return body ? `await eslintPlugin({ ${body} })` : 'await eslintPlugin()';
109129
}
110130

111131
async function isRecommended(targetDir: string): Promise<boolean> {
@@ -123,34 +143,9 @@ async function isRecommended(targetDir: string): Promise<boolean> {
123143
}
124144
}
125145

126-
/** Omits `eslintrc` for standard config filenames (ESLint discovers them automatically). */
127-
function resolveEslintrc(value: PluginAnswer | undefined): string {
128-
if (typeof value !== 'string' || !value) {
129-
return '';
130-
}
131-
if (ESLINT_CONFIG_PATTERN.test(value)) {
132-
return '';
133-
}
134-
return `eslintrc: ${singleQuote(value)}`;
135-
}
136-
137-
/** Formats patterns as a string or array literal, omitting the plugin default. */
138-
function resolvePatterns(value: PluginAnswer | undefined): string {
139-
if (typeof value === 'string') {
140-
return resolvePatterns(value.split(','));
141-
}
142-
if (!Array.isArray(value)) {
143-
return '';
144-
}
145-
const patterns = value
146-
.map(s => s.trim())
147-
.filter(s => s !== '' && s !== DEFAULT_PATTERN)
148-
.map(singleQuote);
149-
if (patterns.length === 0) {
150-
return '';
151-
}
152-
if (patterns.length === 1) {
153-
return `patterns: ${patterns.join('')}`;
154-
}
155-
return `patterns: [${patterns.join(', ')}]`;
146+
async function detectEslintConfig(
147+
targetDir: string,
148+
): Promise<string | undefined> {
149+
const files = await readdir(targetDir, { encoding: 'utf8' });
150+
return files.find(file => ESLINT_CONFIG_PATTERN.test(file));
156151
}

packages/utils/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ export {
194194
MONOREPO_TOOLS,
195195
type MonorepoTool,
196196
} from './lib/monorepo.js';
197+
export {
198+
answerArray,
199+
answerBoolean,
200+
answerString,
201+
} from './lib/plugin-answers.js';
197202
export {
198203
hasCodePushUpDependency,
199204
hasDependency,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { PluginAnswer } from '@code-pushup/models';
2+
3+
/** Extracts a string value from a plugin answer, defaulting to `''`. */
4+
export function answerString(
5+
answers: Record<string, PluginAnswer>,
6+
key: string,
7+
): string {
8+
const value = answers[key];
9+
return typeof value === 'string' ? value : '';
10+
}
11+
12+
/** Extracts a string array from a plugin answer, splitting CSV strings as fallback. */
13+
export function answerArray(
14+
answers: Record<string, PluginAnswer>,
15+
key: string,
16+
): string[] {
17+
const value = answers[key];
18+
if (Array.isArray(value)) {
19+
return value;
20+
}
21+
return (typeof value === 'string' ? value : '')
22+
.split(',')
23+
.map(item => item.trim())
24+
.filter(Boolean);
25+
}
26+
27+
/** Extracts a boolean from a plugin answer, defaulting to `true`. */
28+
export function answerBoolean(
29+
answers: Record<string, PluginAnswer>,
30+
key: string,
31+
): boolean {
32+
return answers[key] !== false;
33+
}

0 commit comments

Comments
 (0)