Skip to content

Commit a23ed2d

Browse files
committed
refactor(plugin-eslint): improve plugin setup binding
1 parent 0b4c70b commit a23ed2d

File tree

9 files changed

+71
-39
lines changed

9 files changed

+71
-39
lines changed

packages/create-cli/README.md

Lines changed: 5 additions & 5 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`** | `'yes'` \| `'no'` | `yes` | 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 recommended categories |
3939

4040
### Examples
4141

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { checkbox, input, select } from '@inquirer/prompts';
1+
import { checkbox, confirm, input, select } from '@inquirer/prompts';
22
import { asyncSequential } from '@code-pushup/utils';
33
import type {
44
CliArgs,
5+
PluginAnswer,
56
PluginPromptDescriptor,
67
PluginSetupBinding,
78
} from './types.js';
@@ -64,7 +65,7 @@ async function detectRecommended(
6465
export async function promptPluginOptions(
6566
descriptors: PluginPromptDescriptor[],
6667
cliArgs: CliArgs,
67-
): Promise<Record<string, string | string[]>> {
68+
): Promise<Record<string, PluginAnswer>> {
6869
const fallback = cliArgs['yes']
6970
? (descriptor: PluginPromptDescriptor) => descriptor.default
7071
: runPrompt;
@@ -76,14 +77,17 @@ export async function promptPluginOptions(
7677
return Object.fromEntries(entries);
7778
}
7879

79-
function cliValue(key: string, cliArgs: CliArgs): string | undefined {
80+
function cliValue(key: string, cliArgs: CliArgs): PluginAnswer | undefined {
8081
const value = cliArgs[key];
81-
return typeof value === 'string' ? value : undefined;
82+
if (typeof value === 'string' || typeof value === 'boolean') {
83+
return value;
84+
}
85+
return undefined;
8286
}
8387

8488
async function runPrompt(
8589
descriptor: PluginPromptDescriptor,
86-
): Promise<string | string[]> {
90+
): Promise<PluginAnswer> {
8791
switch (descriptor.type) {
8892
case 'input':
8993
return input({
@@ -100,5 +104,10 @@ async function runPrompt(
100104
message: descriptor.message,
101105
choices: [...descriptor.choices],
102106
});
107+
case 'confirm':
108+
return confirm({
109+
message: descriptor.message,
110+
default: descriptor.default,
111+
});
103112
}
104113
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { MonorepoTool } from '@code-pushup/utils';
33

44
export type {
55
ImportDeclarationStructure,
6+
PluginAnswer,
67
PluginCodegenResult,
78
PluginPromptDescriptor,
89
PluginSetupBinding,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ describe('runSetupWizard', () => {
273273
'target-dir': outputDir,
274274
'eslint.eslintrc': 'custom-eslint.config.js',
275275
'eslint.patterns': 'src',
276-
'eslint.categories': 'no',
276+
'eslint.categories': false,
277277
});
278278

279279
await expect(

packages/models/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export {
114114
} from './lib/plugin-config.js';
115115
export type {
116116
ImportDeclarationStructure,
117+
PluginAnswer,
117118
PluginCodegenResult,
118119
PluginPromptDescriptor,
119120
PluginSetupBinding,

packages/models/src/lib/plugin-setup.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,17 @@ type CheckboxPrompt<T extends string = string> = PromptBase & {
2525
default: T[];
2626
};
2727

28+
type ConfirmPrompt = PromptBase & {
29+
type: 'confirm';
30+
default: boolean;
31+
};
32+
2833
/** Declarative prompt definition used to collect plugin-specific options. */
2934
export type PluginPromptDescriptor =
3035
| InputPrompt
3136
| SelectPrompt
32-
| CheckboxPrompt;
37+
| CheckboxPrompt
38+
| ConfirmPrompt;
3339

3440
export type ImportDeclarationStructure = {
3541
moduleSpecifier: string;
@@ -38,6 +44,9 @@ export type ImportDeclarationStructure = {
3844
isTypeOnly?: boolean;
3945
};
4046

47+
/** A single value in the answers record produced by plugin prompts. */
48+
export type PluginAnswer = string | string[] | boolean;
49+
4150
/** Import declarations and plugin initialization code produced by `generateConfig`. */
4251
export type PluginCodegenResult = {
4352
imports: ImportDeclarationStructure[];
@@ -61,6 +70,6 @@ export type PluginSetupBinding = {
6170
prompts?: (targetDir: string) => Promise<PluginPromptDescriptor[]>;
6271
isRecommended?: (targetDir: string) => Promise<boolean>;
6372
generateConfig: (
64-
answers: Record<string, string | string[]>,
73+
answers: Record<string, PluginAnswer>,
6574
) => PluginCodegenResult;
6675
};

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

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
11
import { readdir } from 'node:fs/promises';
2+
import { createRequire } from 'node:module';
23
import path from 'node:path';
3-
import type { CategoryConfig, PluginSetupBinding } from '@code-pushup/models';
4-
import { directoryExists, readJsonFile, singleQuote } from '@code-pushup/utils';
4+
import type {
5+
CategoryConfig,
6+
PluginAnswer,
7+
PluginSetupBinding,
8+
} from '@code-pushup/models';
9+
import {
10+
directoryExists,
11+
hasDependency,
12+
readJsonFile,
13+
singleQuote,
14+
} from '@code-pushup/utils';
515
import {
616
DEFAULT_PATTERN,
717
ESLINT_PLUGIN_SLUG,
818
ESLINT_PLUGIN_TITLE,
919
} from './constants.js';
1020

11-
const PACKAGE_NAME = '@code-pushup/eslint-plugin';
21+
const { name: PACKAGE_NAME } = createRequire(import.meta.url)(
22+
'../../package.json',
23+
) as typeof import('../../package.json');
24+
1225
const ESLINT_CONFIG_PATTERN = /^(\.eslintrc(\.\w+)?|eslint\.config\.\w+)$/;
1326

1427
const ESLINT_CATEGORIES: CategoryConfig[] = [
@@ -64,16 +77,12 @@ export const eslintSetupBinding = {
6477
{
6578
key: 'eslint.categories',
6679
message: 'Add recommended categories (bug prevention, code style)?',
67-
type: 'select',
68-
choices: [
69-
{ name: 'Yes', value: 'yes' },
70-
{ name: 'No', value: 'no' },
71-
],
72-
default: 'yes',
80+
type: 'confirm',
81+
default: true,
7382
},
7483
],
75-
generateConfig: (answers: Record<string, string | string[]>) => {
76-
const withCategories = answers['eslint.categories'] !== 'no';
84+
generateConfig: (answers: Record<string, PluginAnswer>) => {
85+
const withCategories = answers['eslint.categories'] !== false;
7786
const args = [
7887
resolveEslintrc(answers['eslint.eslintrc']),
7988
resolvePatterns(answers['eslint.patterns']),
@@ -108,17 +117,14 @@ async function isRecommended(targetDir: string): Promise<boolean> {
108117
dependencies?: Record<string, string>;
109118
devDependencies?: Record<string, string>;
110119
}>(path.join(targetDir, 'package.json'));
111-
return (
112-
'eslint' in (packageJson.dependencies ?? {}) ||
113-
'eslint' in (packageJson.devDependencies ?? {})
114-
);
120+
return hasDependency(packageJson, 'eslint');
115121
} catch {
116122
return false;
117123
}
118124
}
119125

120126
/** Omits `eslintrc` for standard config filenames (ESLint discovers them automatically). */
121-
function resolveEslintrc(value: string | string[] | undefined): string {
127+
function resolveEslintrc(value: PluginAnswer | undefined): string {
122128
if (typeof value !== 'string' || !value) {
123129
return '';
124130
}
@@ -129,9 +135,14 @@ function resolveEslintrc(value: string | string[] | undefined): string {
129135
}
130136

131137
/** Formats patterns as a string or array literal, omitting the plugin default. */
132-
function resolvePatterns(value: string | string[] | undefined): string {
133-
const items = typeof value === 'string' ? value.split(',') : (value ?? []);
134-
const patterns = items
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
135146
.map(s => s.trim())
136147
.filter(s => s !== '' && s !== DEFAULT_PATTERN)
137148
.map(singleQuote);

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ describe('eslintSetupBinding', () => {
114114
eslintSetupBinding.generateConfig({
115115
'eslint.eslintrc': 'eslint.config.ts',
116116
'eslint.patterns': 'src',
117-
'eslint.categories': 'yes',
117+
'eslint.categories': true,
118118
}).pluginInit,
119119
).toBe("await eslintPlugin({ patterns: 'src' })");
120120
});
@@ -124,7 +124,7 @@ describe('eslintSetupBinding', () => {
124124
eslintSetupBinding.generateConfig({
125125
'eslint.eslintrc': 'configs/eslint.config.js',
126126
'eslint.patterns': 'src',
127-
'eslint.categories': 'no',
127+
'eslint.categories': false,
128128
}).pluginInit,
129129
).toBe(
130130
"await eslintPlugin({ eslintrc: 'configs/eslint.config.js', patterns: 'src' })",
@@ -136,7 +136,7 @@ describe('eslintSetupBinding', () => {
136136
eslintSetupBinding.generateConfig({
137137
'eslint.eslintrc': '',
138138
'eslint.patterns': 'src, lib',
139-
'eslint.categories': 'no',
139+
'eslint.categories': false,
140140
}).pluginInit,
141141
).toBe("await eslintPlugin({ patterns: ['src', 'lib'] })");
142142
});
@@ -146,7 +146,7 @@ describe('eslintSetupBinding', () => {
146146
eslintSetupBinding.generateConfig({
147147
'eslint.eslintrc': '',
148148
'eslint.patterns': '',
149-
'eslint.categories': 'no',
149+
'eslint.categories': false,
150150
}).pluginInit,
151151
).toBe('await eslintPlugin()');
152152
});
@@ -156,7 +156,7 @@ describe('eslintSetupBinding', () => {
156156
eslintSetupBinding.generateConfig({
157157
'eslint.eslintrc': '',
158158
'eslint.patterns': '',
159-
'eslint.categories': 'yes',
159+
'eslint.categories': true,
160160
}).categories,
161161
).toHaveLength(2);
162162
});
@@ -166,7 +166,7 @@ describe('eslintSetupBinding', () => {
166166
eslintSetupBinding.generateConfig({
167167
'eslint.eslintrc': '',
168168
'eslint.patterns': '',
169-
'eslint.categories': 'no',
169+
'eslint.categories': false,
170170
}).categories,
171171
).toBeUndefined();
172172
});
@@ -176,7 +176,7 @@ describe('eslintSetupBinding', () => {
176176
eslintSetupBinding.generateConfig({
177177
'eslint.eslintrc': '',
178178
'eslint.patterns': '',
179-
'eslint.categories': 'no',
179+
'eslint.categories': false,
180180
}).imports,
181181
).toEqual([
182182
{

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ export {
196196
} from './lib/monorepo.js';
197197
export {
198198
hasCodePushUpDependency,
199+
hasDependency,
199200
hasScript,
200201
hasWorkspacesEnabled,
201202
listPackages,

0 commit comments

Comments
 (0)