Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ export default tseslint.config(
},
{
sourceTag: 'scope:tooling',
onlyDependOnLibsWithTags: ['scope:tooling', 'scope:shared'],
onlyDependOnLibsWithTags: [
'scope:tooling',
'scope:core',
'scope:plugin',
'scope:shared',
],
},
{
sourceTag: 'type:e2e',
Expand Down
12 changes: 12 additions & 0 deletions packages/create-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ The wizard will prompt you to select plugins and configure their options, then g
| **`--dry-run`** | `boolean` | `false` | Preview changes without writing files |
| **`--yes`**, `-y` | `boolean` | `false` | Skip prompts and use defaults |

### Plugin options

Each plugin exposes its own configuration keys that can be passed as CLI arguments to skip the corresponding prompts.

#### ESLint

| Option | Type | Default | Description |
| ------------------------- | ----------------- | ------------- | -------------------------- |
| **`--eslint.eslintrc`** | `string` | auto-detected | Path to ESLint config |
| **`--eslint.patterns`** | `string` | `src` or `.` | File patterns to lint |
| **`--eslint.categories`** | `'yes'` \| `'no'` | `yes` | Add recommended categories |
Comment thread
matejchalk marked this conversation as resolved.
Outdated

### Examples

Run interactively (default):
Expand Down
1 change: 1 addition & 0 deletions packages/create-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
},
"type": "module",
"dependencies": {
"@code-pushup/eslint-plugin": "0.119.0",
"@code-pushup/models": "0.119.0",
"@code-pushup/utils": "0.119.0",
"@inquirer/prompts": "^8.0.0",
Expand Down
5 changes: 3 additions & 2 deletions packages/create-cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#! /usr/bin/env node
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { eslintSetupBinding } from '@code-pushup/eslint-plugin';
import { parsePluginSlugs, validatePluginSlugs } from './lib/setup/plugins.js';
import {
CI_PROVIDERS,
Expand All @@ -10,8 +11,8 @@ import {
} from './lib/setup/types.js';
import { runSetupWizard } from './lib/setup/wizard.js';

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

const argv = await yargs(hideBin(process.argv))
.option('dry-run', {
Expand Down
74 changes: 8 additions & 66 deletions packages/create-cli/src/lib/setup/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import type { CategoryConfig, PluginMeta } from '@code-pushup/models';
import type { PluginCodegenResult } from '@code-pushup/models';
import type { MonorepoTool } from '@code-pushup/utils';

export type {
ImportDeclarationStructure,
PluginCodegenResult,
PluginPromptDescriptor,
PluginSetupBinding,
} from '@code-pushup/models';

export const CI_PROVIDERS = ['github', 'gitlab', 'none'] as const;
export type CiProvider = (typeof CI_PROVIDERS)[number];

Expand All @@ -24,50 +31,6 @@ export type CliArgs = {
[key: string]: unknown;
};

type PromptBase = {
key: string;
message: string;
};

type PromptChoice<T extends string> = { name: string; value: T };

type InputPrompt = PromptBase & {
type: 'input';
default: string;
};

type SelectPrompt<T extends string = string> = PromptBase & {
type: 'select';
choices: PromptChoice<T>[];
default: T;
};

type CheckboxPrompt<T extends string = string> = PromptBase & {
type: 'checkbox';
choices: PromptChoice<T>[];
default: T[];
};

/** Declarative prompt definition used to collect plugin-specific options. */
export type PluginPromptDescriptor =
| InputPrompt
| SelectPrompt
| CheckboxPrompt;

export type ImportDeclarationStructure = {
moduleSpecifier: string;
defaultImport?: string;
namedImports?: string[];
isTypeOnly?: boolean;
};

/** Import declarations and plugin initialization code produced by `generateConfig`. */
export type PluginCodegenResult = {
imports: ImportDeclarationStructure[];
pluginInit: string;
categories?: CategoryConfig[];
};

export type ScopedPluginResult = {
scope: PluginScope;
result: PluginCodegenResult;
Expand All @@ -79,27 +42,6 @@ export type ConfigContext = {
tool: MonorepoTool | null;
};

/**
* Defines how a plugin integrates with the setup wizard.
*
* Each supported plugin provides a binding that controls:
* - Pre-selection: `isRecommended` detects if the plugin is relevant for the repository
* - Configuration: `prompts` collect plugin-specific options interactively
* - Code generation: `generateConfig` produces the import and initialization code
*/
export type PluginSetupBinding = {
slug: PluginMeta['slug'];
title: PluginMeta['title'];
packageName: NonNullable<PluginMeta['packageName']>;
prompts?: PluginPromptDescriptor[];
scope?: PluginScope;
isRecommended?: (targetDir: string) => Promise<boolean>;
generateConfig: (
answers: Record<string, string | string[]>,
context: ConfigContext,
) => PluginCodegenResult;
};

/** A project discovered in a monorepo workspace. */
export type WizardProject = {
name: string;
Expand Down
70 changes: 69 additions & 1 deletion packages/create-cli/src/lib/setup/wizard.int.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { eslintSetupBinding } from '@code-pushup/eslint-plugin';
import { cleanTestFolder } from '@code-pushup/test-utils';
import { getGitRoot } from '@code-pushup/utils';
import type { PluginSetupBinding } from './types.js';
Expand All @@ -19,7 +20,7 @@ const TEST_BINDINGS: PluginSetupBinding[] = [
title: 'Alpha Plugin',
packageName: '@code-pushup/alpha-plugin',
isRecommended: () => Promise.resolve(true),
prompts: [
prompts: async () => [
{
key: 'alpha.path',
message: 'Path to config',
Expand Down Expand Up @@ -222,4 +223,71 @@ describe('runSetupWizard', () => {
readFile(path.join(outputDir, '.gitignore'), 'utf8'),
).resolves.toBe('node_modules\n.code-pushup\n');
});

it('should generate config with ESLint plugin using defaults', async () => {
await runSetupWizard([eslintSetupBinding], {
yes: true,
plugins: ['eslint'],
'config-format': 'ts',
'target-dir': outputDir,
});

await expect(
readFile(path.join(outputDir, 'code-pushup.config.ts'), 'utf8'),
).resolves.toMatchInlineSnapshot(`
"import eslintPlugin from '@code-pushup/eslint-plugin';
import type { CoreConfig } from '@code-pushup/models';

export default {
plugins: [
await eslintPlugin(),
],
categories: [
{
slug: 'bug-prevention',
title: 'Bug prevention',
description: 'Lint rules that find **potential bugs** in your code.',
refs: [
{ type: 'group', plugin: 'eslint', slug: 'problems', weight: 1 },
],
},
{
slug: 'code-style',
title: 'Code style',
description: 'Lint rules that promote **good practices** and consistency in your code.',
refs: [
{ type: 'group', plugin: 'eslint', slug: 'suggestions', weight: 1 },
],
},
],
} satisfies CoreConfig;
"
`);
});

it('should generate config with custom ESLint options', async () => {
await runSetupWizard([eslintSetupBinding], {
yes: true,
plugins: ['eslint'],
'config-format': 'ts',
'target-dir': outputDir,
'eslint.eslintrc': 'custom-eslint.config.js',
'eslint.patterns': 'src',
'eslint.categories': 'no',
});

await expect(
readFile(path.join(outputDir, 'code-pushup.config.ts'), 'utf8'),
).resolves.toMatchInlineSnapshot(`
"import eslintPlugin from '@code-pushup/eslint-plugin';
import type { CoreConfig } from '@code-pushup/models';

export default {
plugins: [
await eslintPlugin({ eslintrc: 'custom-eslint.config.js', patterns: 'src' }),
],
} satisfies CoreConfig;
"
`);
});
});
15 changes: 8 additions & 7 deletions packages/create-cli/src/lib/setup/wizard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
import { promptPluginOptions, promptPluginSelection } from './prompts.js';
import type {
CliArgs,
ConfigContext,
FileChange,
PluginCodegenResult,
PluginSetupBinding,
Expand Down Expand Up @@ -62,7 +61,7 @@ export async function runSetupWizard(
selectedBindings,
async binding => ({
scope: binding.scope ?? 'project',
result: await resolveBinding(binding, cliArgs, context),
result: await resolveBinding(binding, cliArgs, targetDir),
}),
);

Expand Down Expand Up @@ -103,12 +102,14 @@ export async function runSetupWizard(
async function resolveBinding(
binding: PluginSetupBinding,
cliArgs: CliArgs,
context: ConfigContext,
targetDir: string,
): Promise<PluginCodegenResult> {
const answers = binding.prompts
? await promptPluginOptions(binding.prompts, cliArgs)
: {};
return binding.generateConfig(answers, context);
const descriptors = binding.prompts ? await binding.prompts(targetDir) : [];
const answers =
descriptors.length > 0
? await promptPluginOptions(descriptors, cliArgs)
: {};
return binding.generateConfig(answers);
}

async function writeStandaloneConfig(
Expand Down
6 changes: 6 additions & 0 deletions packages/models/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ export {
type PluginScoreTargets,
type PluginUrls,
} from './lib/plugin-config.js';
export type {
ImportDeclarationStructure,
PluginCodegenResult,
PluginPromptDescriptor,
PluginSetupBinding,
} from './lib/plugin-setup.js';
export {
auditReportSchema,
pluginReportSchema,
Expand Down
66 changes: 66 additions & 0 deletions packages/models/src/lib/plugin-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { CategoryConfig } from './category-config.js';
import type { PluginMeta } from './plugin-config.js';

type PromptBase = {
key: string;
message: string;
};

type PromptChoice<T extends string> = { name: string; value: T };

type InputPrompt = PromptBase & {
type: 'input';
default: string;
};

type SelectPrompt<T extends string = string> = PromptBase & {
type: 'select';
choices: PromptChoice<T>[];
default: T;
};

type CheckboxPrompt<T extends string = string> = PromptBase & {
type: 'checkbox';
choices: PromptChoice<T>[];
default: T[];
};

/** Declarative prompt definition used to collect plugin-specific options. */
export type PluginPromptDescriptor =
| InputPrompt
| SelectPrompt
| CheckboxPrompt;

export type ImportDeclarationStructure = {
moduleSpecifier: string;
defaultImport?: string;
namedImports?: string[];
isTypeOnly?: boolean;
};

/** Import declarations and plugin initialization code produced by `generateConfig`. */
export type PluginCodegenResult = {
imports: ImportDeclarationStructure[];
pluginInit: string;
categories?: CategoryConfig[];
};

/**
* Defines how a plugin integrates with the setup wizard.
*
* Each supported plugin provides a binding that controls:
* - Pre-selection: `isRecommended` detects if the plugin is relevant for the repository
* - Configuration: `prompts` collect plugin-specific options interactively
* - Code generation: `generateConfig` produces the import and initialization code
*/
export type PluginSetupBinding = {
slug: PluginMeta['slug'];
title: PluginMeta['title'];
packageName: NonNullable<PluginMeta['packageName']>;
scope?: 'project' | 'root';
prompts?: (targetDir: string) => Promise<PluginPromptDescriptor[]>;
isRecommended?: (targetDir: string) => Promise<boolean>;
generateConfig: (
answers: Record<string, string | string[]>,
) => PluginCodegenResult;
};
1 change: 1 addition & 0 deletions packages/plugin-eslint/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { eslintPlugin } from './lib/eslint-plugin.js';

export default eslintPlugin;

export { eslintSetupBinding } from './lib/binding.js';
export type { ESLintPluginConfig } from './lib/config.js';
export { ESLINT_PLUGIN_SLUG } from './lib/constants.js';

Expand Down
Loading
Loading