Skip to content

Commit b6103bc

Browse files
authored
feat(plugin-eslint): add setup wizard binding (#1269)
1 parent 9c3fe42 commit b6103bc

File tree

16 files changed

+554
-83
lines changed

16 files changed

+554
-83
lines changed

eslint.config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@ export default tseslint.config(
4545
},
4646
{
4747
sourceTag: 'scope:tooling',
48-
onlyDependOnLibsWithTags: ['scope:tooling', 'scope:shared'],
48+
onlyDependOnLibsWithTags: [
49+
'scope:tooling',
50+
'scope:core',
51+
'scope:plugin',
52+
'scope:shared',
53+
],
4954
},
5055
{
5156
sourceTag: 'type:e2e',

packages/create-cli/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,18 @@ The wizard will prompt you to select plugins and configure their options, then g
2525
| **`--dry-run`** | `boolean` | `false` | Preview changes without writing files |
2626
| **`--yes`**, `-y` | `boolean` | `false` | Skip prompts and use defaults |
2727

28+
### Plugin options
29+
30+
Each plugin exposes its own configuration keys that can be passed as CLI arguments to skip the corresponding prompts.
31+
32+
#### ESLint
33+
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 |
39+
2840
### Examples
2941

3042
Run interactively (default):

packages/create-cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
},
2727
"type": "module",
2828
"dependencies": {
29+
"@code-pushup/eslint-plugin": "0.119.1",
2930
"@code-pushup/models": "0.119.1",
3031
"@code-pushup/utils": "0.119.1",
3132
"@inquirer/prompts": "^8.0.0",

packages/create-cli/src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#! /usr/bin/env node
22
import yargs from 'yargs';
33
import { hideBin } from 'yargs/helpers';
4+
import { eslintSetupBinding } from '@code-pushup/eslint-plugin';
45
import { parsePluginSlugs, validatePluginSlugs } from './lib/setup/plugins.js';
56
import {
67
CI_PROVIDERS,
@@ -10,8 +11,8 @@ import {
1011
} from './lib/setup/types.js';
1112
import { runSetupWizard } from './lib/setup/wizard.js';
1213

13-
// TODO: create, import and pass plugin bindings (eslint, coverage, lighthouse, typescript, js-packages, jsdocs, axe)
14-
const bindings: PluginSetupBinding[] = [];
14+
// TODO: create, import and pass remaining plugin bindings (coverage, lighthouse, typescript, js-packages, jsdocs, axe)
15+
const bindings: PluginSetupBinding[] = [eslintSetupBinding];
1516

1617
const argv = await yargs(hideBin(process.argv))
1718
.option('dry-run', {

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: 9 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
import type { CategoryConfig, PluginMeta } from '@code-pushup/models';
1+
import type { PluginCodegenResult } from '@code-pushup/models';
22
import type { MonorepoTool } from '@code-pushup/utils';
33

4+
export type {
5+
ImportDeclarationStructure,
6+
PluginAnswer,
7+
PluginCodegenResult,
8+
PluginPromptDescriptor,
9+
PluginSetupBinding,
10+
} from '@code-pushup/models';
11+
412
export const CI_PROVIDERS = ['github', 'gitlab', 'none'] as const;
513
export type CiProvider = (typeof CI_PROVIDERS)[number];
614

@@ -24,50 +32,6 @@ export type CliArgs = {
2432
[key: string]: unknown;
2533
};
2634

27-
type PromptBase = {
28-
key: string;
29-
message: string;
30-
};
31-
32-
type PromptChoice<T extends string> = { name: string; value: T };
33-
34-
type InputPrompt = PromptBase & {
35-
type: 'input';
36-
default: string;
37-
};
38-
39-
type SelectPrompt<T extends string = string> = PromptBase & {
40-
type: 'select';
41-
choices: PromptChoice<T>[];
42-
default: T;
43-
};
44-
45-
type CheckboxPrompt<T extends string = string> = PromptBase & {
46-
type: 'checkbox';
47-
choices: PromptChoice<T>[];
48-
default: T[];
49-
};
50-
51-
/** Declarative prompt definition used to collect plugin-specific options. */
52-
export type PluginPromptDescriptor =
53-
| InputPrompt
54-
| SelectPrompt
55-
| CheckboxPrompt;
56-
57-
export type ImportDeclarationStructure = {
58-
moduleSpecifier: string;
59-
defaultImport?: string;
60-
namedImports?: string[];
61-
isTypeOnly?: boolean;
62-
};
63-
64-
/** Import declarations and plugin initialization code produced by `generateConfig`. */
65-
export type PluginCodegenResult = {
66-
imports: ImportDeclarationStructure[];
67-
pluginInit: string;
68-
categories?: CategoryConfig[];
69-
};
70-
7135
export type ScopedPluginResult = {
7236
scope: PluginScope;
7337
result: PluginCodegenResult;
@@ -79,27 +43,6 @@ export type ConfigContext = {
7943
tool: MonorepoTool | null;
8044
};
8145

82-
/**
83-
* Defines how a plugin integrates with the setup wizard.
84-
*
85-
* Each supported plugin provides a binding that controls:
86-
* - Pre-selection: `isRecommended` detects if the plugin is relevant for the repository
87-
* - Configuration: `prompts` collect plugin-specific options interactively
88-
* - Code generation: `generateConfig` produces the import and initialization code
89-
*/
90-
export type PluginSetupBinding = {
91-
slug: PluginMeta['slug'];
92-
title: PluginMeta['title'];
93-
packageName: NonNullable<PluginMeta['packageName']>;
94-
prompts?: PluginPromptDescriptor[];
95-
scope?: PluginScope;
96-
isRecommended?: (targetDir: string) => Promise<boolean>;
97-
generateConfig: (
98-
answers: Record<string, string | string[]>,
99-
context: ConfigContext,
100-
) => PluginCodegenResult;
101-
};
102-
10346
/** A project discovered in a monorepo workspace. */
10447
export type WizardProject = {
10548
name: string;

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

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { readFile, writeFile } from 'node:fs/promises';
22
import path from 'node:path';
3+
import { eslintSetupBinding } from '@code-pushup/eslint-plugin';
34
import { cleanTestFolder } from '@code-pushup/test-utils';
45
import { getGitRoot } from '@code-pushup/utils';
56
import type { PluginSetupBinding } from './types.js';
@@ -19,7 +20,7 @@ const TEST_BINDINGS: PluginSetupBinding[] = [
1920
title: 'Alpha Plugin',
2021
packageName: '@code-pushup/alpha-plugin',
2122
isRecommended: () => Promise.resolve(true),
22-
prompts: [
23+
prompts: async () => [
2324
{
2425
key: 'alpha.path',
2526
message: 'Path to config',
@@ -222,4 +223,71 @@ describe('runSetupWizard', () => {
222223
readFile(path.join(outputDir, '.gitignore'), 'utf8'),
223224
).resolves.toBe('node_modules\n.code-pushup\n');
224225
});
226+
227+
it('should generate config with ESLint plugin using defaults', async () => {
228+
await runSetupWizard([eslintSetupBinding], {
229+
yes: true,
230+
plugins: ['eslint'],
231+
'config-format': 'ts',
232+
'target-dir': outputDir,
233+
});
234+
235+
await expect(
236+
readFile(path.join(outputDir, 'code-pushup.config.ts'), 'utf8'),
237+
).resolves.toMatchInlineSnapshot(`
238+
"import eslintPlugin from '@code-pushup/eslint-plugin';
239+
import type { CoreConfig } from '@code-pushup/models';
240+
241+
export default {
242+
plugins: [
243+
await eslintPlugin(),
244+
],
245+
categories: [
246+
{
247+
slug: 'bug-prevention',
248+
title: 'Bug prevention',
249+
description: 'Lint rules that find **potential bugs** in your code.',
250+
refs: [
251+
{ type: 'group', plugin: 'eslint', slug: 'problems', weight: 1 },
252+
],
253+
},
254+
{
255+
slug: 'code-style',
256+
title: 'Code style',
257+
description: 'Lint rules that promote **good practices** and consistency in your code.',
258+
refs: [
259+
{ type: 'group', plugin: 'eslint', slug: 'suggestions', weight: 1 },
260+
],
261+
},
262+
],
263+
} satisfies CoreConfig;
264+
"
265+
`);
266+
});
267+
268+
it('should generate config with custom ESLint options', async () => {
269+
await runSetupWizard([eslintSetupBinding], {
270+
yes: true,
271+
plugins: ['eslint'],
272+
'config-format': 'ts',
273+
'target-dir': outputDir,
274+
'eslint.eslintrc': 'custom-eslint.config.js',
275+
'eslint.patterns': 'src',
276+
'eslint.categories': false,
277+
});
278+
279+
await expect(
280+
readFile(path.join(outputDir, 'code-pushup.config.ts'), 'utf8'),
281+
).resolves.toMatchInlineSnapshot(`
282+
"import eslintPlugin from '@code-pushup/eslint-plugin';
283+
import type { CoreConfig } from '@code-pushup/models';
284+
285+
export default {
286+
plugins: [
287+
await eslintPlugin({ eslintrc: 'custom-eslint.config.js', patterns: 'src' }),
288+
],
289+
} satisfies CoreConfig;
290+
"
291+
`);
292+
});
225293
});

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import {
2828
import { promptPluginOptions, promptPluginSelection } from './prompts.js';
2929
import type {
3030
CliArgs,
31-
ConfigContext,
3231
FileChange,
3332
PluginCodegenResult,
3433
PluginSetupBinding,
@@ -62,7 +61,7 @@ export async function runSetupWizard(
6261
selectedBindings,
6362
async binding => ({
6463
scope: binding.scope ?? 'project',
65-
result: await resolveBinding(binding, cliArgs, context),
64+
result: await resolveBinding(binding, cliArgs, targetDir),
6665
}),
6766
);
6867

@@ -103,12 +102,14 @@ export async function runSetupWizard(
103102
async function resolveBinding(
104103
binding: PluginSetupBinding,
105104
cliArgs: CliArgs,
106-
context: ConfigContext,
105+
targetDir: string,
107106
): Promise<PluginCodegenResult> {
108-
const answers = binding.prompts
109-
? await promptPluginOptions(binding.prompts, cliArgs)
110-
: {};
111-
return binding.generateConfig(answers, context);
107+
const descriptors = binding.prompts ? await binding.prompts(targetDir) : [];
108+
const answers =
109+
descriptors.length > 0
110+
? await promptPluginOptions(descriptors, cliArgs)
111+
: {};
112+
return binding.generateConfig(answers);
112113
}
113114

114115
async function writeStandaloneConfig(

packages/models/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ export {
112112
type PluginScoreTargets,
113113
type PluginUrls,
114114
} from './lib/plugin-config.js';
115+
export type {
116+
ImportDeclarationStructure,
117+
PluginAnswer,
118+
PluginCodegenResult,
119+
PluginPromptDescriptor,
120+
PluginSetupBinding,
121+
} from './lib/plugin-setup.js';
115122
export {
116123
auditReportSchema,
117124
pluginReportSchema,

0 commit comments

Comments
 (0)