From 1795a11805997706f922a783a5bd268e89ab2418 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Mon, 30 Mar 2026 15:55:39 -0400 Subject: [PATCH 1/4] refactor(plugin-lighthouse): consolidate category definitions in binding --- packages/plugin-lighthouse/src/lib/binding.ts | 58 +++++++++---------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/packages/plugin-lighthouse/src/lib/binding.ts b/packages/plugin-lighthouse/src/lib/binding.ts index 3da2bfe6b..681ebf1e1 100644 --- a/packages/plugin-lighthouse/src/lib/binding.ts +++ b/packages/plugin-lighthouse/src/lib/binding.ts @@ -14,7 +14,6 @@ import { LIGHTHOUSE_PLUGIN_SLUG, LIGHTHOUSE_PLUGIN_TITLE, } from './constants.js'; -import type { LighthouseGroupSlug } from './types.js'; const { name: PACKAGE_NAME } = createRequire(import.meta.url)( '../../package.json', @@ -23,51 +22,37 @@ const { name: PACKAGE_NAME } = createRequire(import.meta.url)( const DEFAULT_URL = 'http://localhost:4200'; const PLUGIN_VAR = 'lhPlugin'; -const CATEGORY_TO_GROUP: Record = { - performance: 'performance', - a11y: 'accessibility', - 'best-practices': 'best-practices', - seo: 'seo', -}; - -const CATEGORIES: CategoryCodegenConfig[] = [ +const CATEGORIES = [ { slug: 'performance', title: 'Performance', description: 'Measure performance and find opportunities to speed up page loads.', - refsExpression: `lighthouseGroupRefs(${PLUGIN_VAR}, 'performance')`, + group: 'performance', }, { slug: 'a11y', title: 'Accessibility', description: 'Determine if all users access content and navigate your site effectively.', - refsExpression: `lighthouseGroupRefs(${PLUGIN_VAR}, 'accessibility')`, + group: 'accessibility', }, { slug: 'best-practices', title: 'Best Practices', description: 'Improve code health of your web page following these best practices.', - refsExpression: `lighthouseGroupRefs(${PLUGIN_VAR}, 'best-practices')`, + group: 'best-practices', }, { slug: 'seo', title: 'SEO', description: 'Ensure that your page is optimized for search engine results ranking.', - refsExpression: `lighthouseGroupRefs(${PLUGIN_VAR}, 'seo')`, + group: 'seo', }, ]; -const CATEGORY_CHOICES = CATEGORIES.map(({ slug, title }) => ({ - name: title, - value: slug, -})); - -const DEFAULT_CATEGORIES = CATEGORIES.map(({ slug }) => slug); - type LighthouseOptions = { urls: [string, ...string[]]; categories: string[]; @@ -88,8 +73,11 @@ export const lighthouseSetupBinding = { key: 'lighthouse.categories', message: 'Lighthouse categories', type: 'checkbox', - choices: [...CATEGORY_CHOICES], - default: [...DEFAULT_CATEGORIES], + choices: CATEGORIES.map(({ slug, title }) => ({ + name: title, + value: slug, + })), + default: CATEGORIES.map(({ slug }) => slug), }, ], generateConfig: (answers: Record) => { @@ -133,10 +121,9 @@ function parseAnswers( function formatPluginCall({ urls, categories }: LighthouseOptions): string { const formattedUrls = formatUrls(urls); - const groups = categories.flatMap(slug => { - const group = CATEGORY_TO_GROUP[slug]; - return group ? [group] : []; - }); + const groups = CATEGORIES.filter(({ slug }) => categories.includes(slug)).map( + ({ group }) => group, + ); if (groups.length === 0 || groups.length === LIGHTHOUSE_GROUP_SLUGS.length) { return `lighthousePlugin(${formattedUrls})`; } @@ -144,15 +131,22 @@ function formatPluginCall({ urls, categories }: LighthouseOptions): string { return `lighthousePlugin(${formattedUrls}, { onlyGroups: [${onlyGroups}] })`; } +function createCategories({ + categories, +}: LighthouseOptions): CategoryCodegenConfig[] { + return CATEGORIES.filter(({ slug }) => categories.includes(slug)).map( + ({ slug, title, description, group }) => ({ + slug, + title, + description, + refsExpression: `lighthouseGroupRefs(${PLUGIN_VAR}, ${singleQuote(group)})`, + }), + ); +} + function formatUrls([first, ...rest]: [string, ...string[]]): string { if (rest.length === 0) { return singleQuote(first); } return `[${[first, ...rest].map(singleQuote).join(', ')}]`; } - -function createCategories({ - categories, -}: LighthouseOptions): CategoryCodegenConfig[] { - return CATEGORIES.filter(({ slug }) => categories.includes(slug)); -} From 72927ac56f5af5e97ab077432be5e7786cc48b36 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Mon, 30 Mar 2026 15:57:22 -0400 Subject: [PATCH 2/4] refactor(plugin-js-packages): extract silent package manager detection --- .../plugin-js-packages/src/lib/binding.ts | 17 ++---- .../derive-package-manager.ts | 57 ++++++++++++------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/packages/plugin-js-packages/src/lib/binding.ts b/packages/plugin-js-packages/src/lib/binding.ts index fe559b8db..d3090c997 100644 --- a/packages/plugin-js-packages/src/lib/binding.ts +++ b/packages/plugin-js-packages/src/lib/binding.ts @@ -12,14 +12,13 @@ import { fileExists, singleQuote, } from '@code-pushup/utils'; -import type { PackageManagerId } from './config.js'; import { DEFAULT_CHECKS, DEFAULT_DEPENDENCY_GROUPS, JS_PACKAGES_PLUGIN_SLUG, JS_PACKAGES_PLUGIN_TITLE, } from './constants.js'; -import { derivePackageManager } from './package-managers/derive-package-manager.js'; +import { detectPackageManager } from './package-managers/derive-package-manager.js'; const { name: PACKAGE_NAME } = createRequire(import.meta.url)( '../../package.json', @@ -73,7 +72,9 @@ export const jsPackagesSetupBinding = { packageName: PACKAGE_NAME, isRecommended, prompts: async (targetDir: string) => { - const packageManager = await detectPackageManager(targetDir); + const packageManager = await detectPackageManager(targetDir).catch( + () => DEFAULT_PACKAGE_MANAGER, + ); return [ { key: 'js-packages.packageManager', @@ -175,13 +176,3 @@ function createCategories({ async function isRecommended(targetDir: string): Promise { return fileExists(path.join(targetDir, 'package.json')); } - -async function detectPackageManager( - targetDir: string, -): Promise { - try { - return await derivePackageManager(targetDir); - } catch { - return DEFAULT_PACKAGE_MANAGER; - } -} diff --git a/packages/plugin-js-packages/src/lib/package-managers/derive-package-manager.ts b/packages/plugin-js-packages/src/lib/package-managers/derive-package-manager.ts index 2906d6b21..cf2082d0b 100644 --- a/packages/plugin-js-packages/src/lib/package-managers/derive-package-manager.ts +++ b/packages/plugin-js-packages/src/lib/package-managers/derive-package-manager.ts @@ -31,34 +31,41 @@ export async function derivePackageManagerInPackageJson( return false; } -export async function derivePackageManager( - currentDir = process.cwd(), -): Promise { +type DetectionResult = { + id: PackageManagerId; + sourceDescription: string; +}; + +async function resolvePackageManager( + currentDir: string, +): Promise { const pkgManagerFromPackageJson = await derivePackageManagerInPackageJson(currentDir); if (pkgManagerFromPackageJson) { - logDerivedPackageManager( - pkgManagerFromPackageJson, - 'packageManager field in package.json', - ); - return pkgManagerFromPackageJson; + return { + id: pkgManagerFromPackageJson, + sourceDescription: 'packageManager field in package.json', + }; } // Check for lock files if (await fileExists(path.join(currentDir, 'package-lock.json'))) { - logDerivedPackageManager('npm', 'existence of package-lock.json file'); - return 'npm'; + return { + id: 'npm', + sourceDescription: 'existence of package-lock.json file', + }; } else if (await fileExists(path.join(currentDir, 'pnpm-lock.yaml'))) { - logDerivedPackageManager('pnpm', 'existence of pnpm-lock.yaml file'); - return 'pnpm'; + return { + id: 'pnpm', + sourceDescription: 'existence of pnpm-lock.yaml file', + }; } else if (await fileExists(path.join(currentDir, 'yarn.lock'))) { const yarnVersion = await deriveYarnVersion(); if (yarnVersion) { - logDerivedPackageManager( - yarnVersion, - 'existence of yarn.lock file and yarn -v command', - ); - return yarnVersion; + return { + id: yarnVersion, + sourceDescription: 'existence of yarn.lock file and yarn -v command', + }; } } @@ -67,14 +74,22 @@ export async function derivePackageManager( ); } -function logDerivedPackageManager( - id: PackageManagerId, - sourceDescription: string, -): void { +export async function detectPackageManager( + currentDir = process.cwd(), +): Promise { + const { id } = await resolvePackageManager(currentDir); + return id; +} + +export async function derivePackageManager( + currentDir = process.cwd(), +): Promise { + const { id, sourceDescription } = await resolvePackageManager(currentDir); const pm = packageManagers[id]; logger.info( formatMetaLog( `Inferred ${pm.name} package manager from ${sourceDescription}`, ), ); + return id; } From 9443de7d61e8b5a89e43a543ef25e06405051d5b Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Mon, 30 Mar 2026 15:59:03 -0400 Subject: [PATCH 3/4] refactor(create-cli): consolidate wizard logging --- packages/create-cli/README.md | 30 ++--- packages/create-cli/src/lib/setup/wizard.ts | 49 ++++---- .../src/lib/setup/wizard.unit.test.ts | 113 ++++++++++-------- packages/plugin-axe/src/lib/binding.ts | 6 +- packages/plugin-coverage/src/lib/binding.ts | 12 +- packages/plugin-eslint/src/lib/binding.ts | 6 +- .../plugin-js-packages/src/lib/binding.ts | 8 +- packages/plugin-jsdocs/src/lib/binding.ts | 34 +++--- packages/plugin-lighthouse/src/lib/binding.ts | 4 +- packages/plugin-typescript/src/lib/binding.ts | 4 +- 10 files changed, 140 insertions(+), 126 deletions(-) diff --git a/packages/create-cli/README.md b/packages/create-cli/README.md index d1c4bf8e1..9dff203ed 100644 --- a/packages/create-cli/README.md +++ b/packages/create-cli/README.md @@ -35,7 +35,7 @@ Each plugin exposes its own configuration keys that can be passed as CLI argumen | ------------------------- | --------- | ------------- | --------------------- | | **`--eslint.eslintrc`** | `string` | auto-detected | Path to ESLint config | | **`--eslint.patterns`** | `string` | `src` or `.` | File patterns to lint | -| **`--eslint.categories`** | `boolean` | `true` | Add ESLint categories | +| **`--eslint.categories`** | `boolean` | `true` | Add categories | #### Coverage @@ -47,37 +47,37 @@ Each plugin exposes its own configuration keys that can be passed as CLI argumen | **`--coverage.testCommand`** | `string` | auto-detected | Command to run tests | | **`--coverage.types`** | `('function'` \| `'branch'` \| `'line')[]` | all | Coverage types to measure | | **`--coverage.continueOnFail`** | `boolean` | `true` | Continue if test command fails | -| **`--coverage.categories`** | `boolean` | `true` | Add Code coverage categories | +| **`--coverage.categories`** | `boolean` | `true` | Add categories | #### JS Packages -| Option | Type | Default | Description | -| ------------------------------------ | ---------------------------------------------------------- | ------------- | -------------------------- | -| **`--js-packages.packageManager`** | `'npm'` \| `'yarn-classic'` \| `'yarn-modern'` \| `'pnpm'` | auto-detected | Package manager | -| **`--js-packages.checks`** | `('audit'` \| `'outdated')[]` | both | Checks to run | -| **`--js-packages.dependencyGroups`** | `('prod'` \| `'dev'` \| `'optional')[]` | `prod`, `dev` | Dependency groups | -| **`--js-packages.categories`** | `boolean` | `true` | Add JS packages categories | +| Option | Type | Default | Description | +| ------------------------------------ | ---------------------------------------------------------- | ------------- | ----------------- | +| **`--js-packages.packageManager`** | `'npm'` \| `'yarn-classic'` \| `'yarn-modern'` \| `'pnpm'` | auto-detected | Package manager | +| **`--js-packages.checks`** | `('audit'` \| `'outdated')[]` | both | Checks to run | +| **`--js-packages.dependencyGroups`** | `('prod'` \| `'dev'` \| `'optional')[]` | `prod`, `dev` | Dependency groups | +| **`--js-packages.categories`** | `boolean` | `true` | Add categories | #### TypeScript -| Option | Type | Default | Description | -| ----------------------------- | --------- | ------------- | ------------------------- | -| **`--typescript.tsconfig`** | `string` | auto-detected | TypeScript config file | -| **`--typescript.categories`** | `boolean` | `true` | Add TypeScript categories | +| Option | Type | Default | Description | +| ----------------------------- | --------- | ------------- | ---------------------- | +| **`--typescript.tsconfig`** | `string` | auto-detected | TypeScript config file | +| **`--typescript.categories`** | `boolean` | `true` | Add categories | #### Lighthouse | Option | Type | Default | Description | | ----------------------------- | ---------------------------------------------------------------- | ----------------------- | ------------------------------- | | **`--lighthouse.urls`** | `string \| string[]` | `http://localhost:4200` | Target URL(s) (comma-separated) | -| **`--lighthouse.categories`** | `('performance'` \| `'a11y'` \| `'best-practices'` \| `'seo')[]` | all | Lighthouse categories | +| **`--lighthouse.categories`** | `('performance'` \| `'a11y'` \| `'best-practices'` \| `'seo')[]` | all | Categories | #### JSDocs | Option | Type | Default | Description | | ------------------------- | -------------------- | -------------------------------------------- | -------------------------------------- | | **`--jsdocs.patterns`** | `string \| string[]` | `src/**/*.ts, src/**/*.js, !**/node_modules` | Source file patterns (comma-separated) | -| **`--jsdocs.categories`** | `boolean` | `true` | Add JSDocs categories | +| **`--jsdocs.categories`** | `boolean` | `true` | Add categories | #### Axe @@ -86,7 +86,7 @@ Each plugin exposes its own configuration keys that can be passed as CLI argumen | **`--axe.urls`** | `string \| string[]` | `http://localhost:4200` | Target URL(s) (comma-separated) | | **`--axe.preset`** | `'wcag21aa'` \| `'wcag22aa'` \| `'best-practice'` \| `'all'` | `wcag21aa` | Accessibility preset | | **`--axe.setupScript`** | `boolean` | `false` | Create setup script for auth-protected app | -| **`--axe.categories`** | `boolean` | `true` | Add Axe categories | +| **`--axe.categories`** | `boolean` | `true` | Add categories | ### Examples diff --git a/packages/create-cli/src/lib/setup/wizard.ts b/packages/create-cli/src/lib/setup/wizard.ts index af743066f..4fcc62eaa 100644 --- a/packages/create-cli/src/lib/setup/wizard.ts +++ b/packages/create-cli/src/lib/setup/wizard.ts @@ -1,3 +1,4 @@ +import ansis from 'ansis'; import path from 'node:path'; import { type MonorepoTool, @@ -28,7 +29,6 @@ import { import { promptPluginOptions, promptPluginSelection } from './prompts.js'; import type { CliArgs, - FileChange, PluginCodegenResult, PluginSetupBinding, ScopedPluginResult, @@ -83,21 +83,7 @@ export async function runSetupWizard( await resolveGitignore(tree); await resolveCi(tree, ciProvider, context); - logChanges(tree.listChanges()); - - if (cliArgs['dry-run']) { - logger.info('Dry run — no files written.'); - return; - } - - await tree.flush(); - - logger.info('Setup complete.'); - logger.newline(); - logNextSteps([ - ['npx code-pushup', 'Collect your first report'], - ['https://github.com/code-pushup/cli#readme', 'Documentation'], - ]); + await finalize(tree, cliArgs['dry-run']); } async function resolveBinding( @@ -106,7 +92,12 @@ async function resolveBinding( targetDir: string, tree: Pick, ): Promise { - const descriptors = binding.prompts ? await binding.prompts(targetDir) : []; + if (!binding.prompts) { + return binding.generateConfig({}, tree); + } + logger.newline(); + logger.info(ansis.bold(binding.title)); + const descriptors = await binding.prompts(targetDir); const answers = descriptors.length > 0 ? await promptPluginOptions(descriptors, cliArgs) @@ -161,18 +152,32 @@ async function writeMonorepoConfigs( ); } -function logChanges(changes: FileChange[]): void { - changes.forEach(change => { +async function finalize(tree: Tree, dryRun?: boolean): Promise { + logger.newline(); + + tree.listChanges().forEach(change => { logger.info(`${change.type} ${change.path}`); }); -} -function logNextSteps(steps: [string, string][]): void { + if (dryRun) { + logger.newline(); + logger.info('Dry run — no files written.'); + return; + } + + await tree.flush(); + + logger.newline(); + logger.info('Setup complete.'); + logger.newline(); logger.info( formatAsciiTable( { title: 'Next steps:', - rows: steps, + rows: [ + ['npx code-pushup', 'Collect your first report'], + ['https://github.com/code-pushup/cli#readme', 'Documentation'], + ], }, { borderless: true }, ), diff --git a/packages/create-cli/src/lib/setup/wizard.unit.test.ts b/packages/create-cli/src/lib/setup/wizard.unit.test.ts index 04c882946..38852f4e5 100644 --- a/packages/create-cli/src/lib/setup/wizard.unit.test.ts +++ b/packages/create-cli/src/lib/setup/wizard.unit.test.ts @@ -18,21 +18,26 @@ vi.mock('./monorepo.js', async importOriginal => ({ addCodePushUpCommand: vi.fn().mockResolvedValue(undefined), })); -const TEST_BINDING: PluginSetupBinding = { - slug: 'test-plugin', - title: 'Test Plugin', - packageName: '@code-pushup/test-plugin', - isRecommended: () => Promise.resolve(true), - generateConfig: () => ({ - imports: [ - { - moduleSpecifier: '@code-pushup/test-plugin', - defaultImport: 'testPlugin', - }, - ], - pluginInit: ['testPlugin(),'], - }), -}; +function createBinding( + overrides?: Partial, +): PluginSetupBinding { + return { + slug: 'test-plugin', + title: 'Test Plugin', + packageName: '@code-pushup/test-plugin', + isRecommended: () => Promise.resolve(true), + generateConfig: () => ({ + imports: [ + { + moduleSpecifier: '@code-pushup/test-plugin', + defaultImport: 'testPlugin', + }, + ], + pluginInit: ['testPlugin(),'], + }), + ...overrides, + }; +} describe('runSetupWizard', () => { describe('TypeScript config', () => { @@ -41,7 +46,7 @@ describe('runSetupWizard', () => { }); it('should generate ts config and log success', async () => { - await runSetupWizard([TEST_BINDING], { + await runSetupWizard([createBinding()], { yes: true, 'target-dir': MEMFS_VOLUME, }); @@ -67,7 +72,7 @@ describe('runSetupWizard', () => { }); it('should log dry-run message without writing files', async () => { - await runSetupWizard([TEST_BINDING], { + await runSetupWizard([createBinding()], { yes: true, 'dry-run': true, 'target-dir': MEMFS_VOLUME, @@ -109,7 +114,7 @@ describe('runSetupWizard', () => { }); it('should generate .mjs config when js format is auto-detected', async () => { - await runSetupWizard([TEST_BINDING], { + await runSetupWizard([createBinding()], { yes: true, 'target-dir': MEMFS_VOLUME, }); @@ -136,7 +141,7 @@ describe('runSetupWizard', () => { MEMFS_VOLUME, ); - await runSetupWizard([TEST_BINDING], { + await runSetupWizard([createBinding()], { yes: true, 'config-format': 'js', 'target-dir': MEMFS_VOLUME, @@ -159,40 +164,26 @@ describe('runSetupWizard', () => { }); }); - describe('Monorepo config', () => { - const PROJECT_BINDING: PluginSetupBinding = { - slug: 'test-plugin', - title: 'Test Plugin', - packageName: '@code-pushup/test-plugin', - isRecommended: () => Promise.resolve(true), - generateConfig: () => ({ - imports: [ - { - moduleSpecifier: '@code-pushup/test-plugin', - defaultImport: 'testPlugin', - }, + it('should log a heading for each plugin with prompts', async () => { + vol.fromJSON({ 'tsconfig.json': '{}' }, MEMFS_VOLUME); + const withPrompts = (title: string) => + createBinding({ + title, + prompts: async () => [ + { key: 'x', message: 'X:', type: 'input', default: '' }, ], - pluginInit: ['testPlugin(),'], - }), - }; - - const ROOT_BINDING: PluginSetupBinding = { - slug: 'root-plugin', - title: 'Root Plugin', - packageName: '@code-pushup/root-plugin', - scope: 'root', - isRecommended: () => Promise.resolve(true), - generateConfig: () => ({ - imports: [ - { - moduleSpecifier: '@code-pushup/root-plugin', - defaultImport: 'rootPlugin', - }, - ], - pluginInit: ['rootPlugin(),'], - }), - }; + }); + + await runSetupWizard( + [withPrompts('Alpha'), createBinding(), withPrompts('Beta')], + { yes: true, 'target-dir': MEMFS_VOLUME }, + ); + expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Alpha')); + expect(logger.info).toHaveBeenCalledWith(expect.stringContaining('Beta')); + }); + + describe('Monorepo config', () => { beforeEach(() => { vol.fromJSON( { @@ -216,7 +207,7 @@ describe('runSetupWizard', () => { }); it('should generate preset and per-project configs', async () => { - await runSetupWizard([PROJECT_BINDING], { + await runSetupWizard([createBinding()], { yes: true, mode: 'monorepo', 'target-dir': MEMFS_VOLUME, @@ -269,7 +260,23 @@ describe('runSetupWizard', () => { }); it('should generate root config for root-scoped plugins', async () => { - await runSetupWizard([PROJECT_BINDING, ROOT_BINDING], { + const rootBinding = createBinding({ + slug: 'root-plugin', + title: 'Root Plugin', + packageName: '@code-pushup/root-plugin', + scope: 'root', + generateConfig: () => ({ + imports: [ + { + moduleSpecifier: '@code-pushup/root-plugin', + defaultImport: 'rootPlugin', + }, + ], + pluginInit: ['rootPlugin(),'], + }), + }); + + await runSetupWizard([createBinding(), rootBinding], { yes: true, mode: 'monorepo', 'target-dir': MEMFS_VOLUME, diff --git a/packages/plugin-axe/src/lib/binding.ts b/packages/plugin-axe/src/lib/binding.ts index d7af969ea..d4c32f547 100644 --- a/packages/plugin-axe/src/lib/binding.ts +++ b/packages/plugin-axe/src/lib/binding.ts @@ -60,13 +60,13 @@ export const axeSetupBinding = { prompts: async () => [ { key: 'axe.urls', - message: 'Target URL(s) (comma-separated)', + message: 'Target URL(s) (comma-separated):', type: 'input', default: DEFAULT_URL, }, { key: 'axe.preset', - message: 'Accessibility preset', + message: 'Accessibility preset:', type: 'select', choices: [...PRESET_CHOICES], default: AXE_DEFAULT_PRESET, @@ -79,7 +79,7 @@ export const axeSetupBinding = { }, { key: 'axe.categories', - message: 'Add Axe categories?', + message: 'Add categories?', type: 'confirm', default: true, }, diff --git a/packages/plugin-coverage/src/lib/binding.ts b/packages/plugin-coverage/src/lib/binding.ts index 187979814..1876a1d13 100644 --- a/packages/plugin-coverage/src/lib/binding.ts +++ b/packages/plugin-coverage/src/lib/binding.ts @@ -81,32 +81,32 @@ export const coverageSetupBinding = { return [ { key: 'coverage.framework', - message: 'Test framework', + message: 'Test framework:', type: 'select', choices: [...FRAMEWORKS], default: framework, }, { key: 'coverage.configFile', - message: 'Path to test config file', + message: 'Path to test config file:', type: 'input', default: configFile ?? '', }, { key: 'coverage.reportPath', - message: 'Path to LCOV report file', + message: 'Path to LCOV report file:', type: 'input', default: framework === 'other' ? '' : DEFAULT_REPORT_PATH, }, { key: 'coverage.testCommand', - message: 'Command to run tests with coverage', + message: 'Command to run tests with coverage:', type: 'input', default: defaultTestCommand(framework), }, { key: 'coverage.types', - message: 'Coverage types to measure', + message: 'Coverage types:', type: 'checkbox', choices: ALL_COVERAGE_TYPES.map(type => ({ name: pluralize(type), @@ -122,7 +122,7 @@ export const coverageSetupBinding = { }, { key: 'coverage.categories', - message: 'Add Code coverage categories?', + message: 'Add categories?', type: 'confirm', default: true, }, diff --git a/packages/plugin-eslint/src/lib/binding.ts b/packages/plugin-eslint/src/lib/binding.ts index d2e72f716..a9df94bb1 100644 --- a/packages/plugin-eslint/src/lib/binding.ts +++ b/packages/plugin-eslint/src/lib/binding.ts @@ -71,13 +71,13 @@ export const eslintSetupBinding = { prompts: async (targetDir: string) => [ { key: 'eslint.eslintrc', - message: 'Path to ESLint config', + message: 'Path to ESLint config:', type: 'input', default: (await detectEslintConfig(targetDir)) ?? '', }, { key: 'eslint.patterns', - message: 'File patterns to lint', + message: 'File patterns to lint:', type: 'input', default: (await directoryExists(path.join(targetDir, 'src'))) ? 'src' @@ -85,7 +85,7 @@ export const eslintSetupBinding = { }, { key: 'eslint.categories', - message: 'Add ESLint categories?', + message: 'Add categories?', type: 'confirm', default: true, }, diff --git a/packages/plugin-js-packages/src/lib/binding.ts b/packages/plugin-js-packages/src/lib/binding.ts index d3090c997..488461105 100644 --- a/packages/plugin-js-packages/src/lib/binding.ts +++ b/packages/plugin-js-packages/src/lib/binding.ts @@ -78,28 +78,28 @@ export const jsPackagesSetupBinding = { return [ { key: 'js-packages.packageManager', - message: 'Package manager', + message: 'Package manager:', type: 'select', choices: [...PACKAGE_MANAGERS], default: packageManager, }, { key: 'js-packages.checks', - message: 'Checks to run', + message: 'Checks to run:', type: 'checkbox', choices: [...CHECKS], default: [...DEFAULT_CHECKS], }, { key: 'js-packages.dependencyGroups', - message: 'Dependency groups', + message: 'Dependency groups:', type: 'checkbox', choices: [...DEPENDENCY_GROUPS], default: [...DEFAULT_DEPENDENCY_GROUPS], }, { key: 'js-packages.categories', - message: 'Add JS packages categories?', + message: 'Add categories?', type: 'confirm', default: true, }, diff --git a/packages/plugin-jsdocs/src/lib/binding.ts b/packages/plugin-jsdocs/src/lib/binding.ts index 76988eb71..872f295e8 100644 --- a/packages/plugin-jsdocs/src/lib/binding.ts +++ b/packages/plugin-jsdocs/src/lib/binding.ts @@ -21,19 +21,21 @@ const DEFAULT_PATTERNS: [string, ...string[]] = [ '!**/node_modules', ]; -const CATEGORY: CategoryConfig = { - slug: 'docs', - title: 'Documentation', - description: 'Measures how much of your code is **documented**.', - refs: [ - { - type: 'group', - plugin: PLUGIN_SLUG, - slug: 'documentation-coverage', - weight: 1, - }, - ], -}; +const CATEGORIES: CategoryConfig[] = [ + { + slug: 'docs', + title: 'Documentation', + description: 'Measures how much of your code is **documented**.', + refs: [ + { + type: 'group', + plugin: PLUGIN_SLUG, + slug: 'documentation-coverage', + weight: 1, + }, + ], + }, +]; type JsDocsOptions = { patterns: [string, ...string[]]; @@ -47,13 +49,13 @@ export const jsDocsSetupBinding = { prompts: async () => [ { key: 'jsdocs.patterns', - message: 'Source file patterns (comma-separated)', + message: 'Source file patterns (comma-separated):', type: 'input', default: DEFAULT_PATTERNS.join(', '), }, { key: 'jsdocs.categories', - message: 'Add JSDocs categories?', + message: 'Add categories?', type: 'confirm', default: true, }, @@ -65,7 +67,7 @@ export const jsDocsSetupBinding = { { moduleSpecifier: PACKAGE_NAME, defaultImport: 'jsDocsPlugin' }, ], pluginInit: formatPluginInit(options.patterns), - ...(options.categories ? { categories: [CATEGORY] } : {}), + ...(options.categories ? { categories: CATEGORIES } : {}), }; }, } satisfies PluginSetupBinding; diff --git a/packages/plugin-lighthouse/src/lib/binding.ts b/packages/plugin-lighthouse/src/lib/binding.ts index 681ebf1e1..a7db333f6 100644 --- a/packages/plugin-lighthouse/src/lib/binding.ts +++ b/packages/plugin-lighthouse/src/lib/binding.ts @@ -65,13 +65,13 @@ export const lighthouseSetupBinding = { prompts: async (_targetDir: string) => [ { key: 'lighthouse.urls', - message: 'Target URL(s) (comma-separated)', + message: 'Target URL(s) (comma-separated):', type: 'input', default: DEFAULT_URL, }, { key: 'lighthouse.categories', - message: 'Lighthouse categories', + message: 'Categories:', type: 'checkbox', choices: CATEGORIES.map(({ slug, title }) => ({ name: title, diff --git a/packages/plugin-typescript/src/lib/binding.ts b/packages/plugin-typescript/src/lib/binding.ts index 3a68502e7..c90f40f2e 100644 --- a/packages/plugin-typescript/src/lib/binding.ts +++ b/packages/plugin-typescript/src/lib/binding.ts @@ -54,13 +54,13 @@ export const typescriptSetupBinding = { return [ { key: 'typescript.tsconfig', - message: 'TypeScript config file', + message: 'TypeScript config file:', type: 'input', default: tsconfig, }, { key: 'typescript.categories', - message: 'Add TypeScript categories?', + message: 'Add categories?', type: 'confirm', default: true, }, From fd039648a47a600c307696166c2c40550d3941b0 Mon Sep 17 00:00:00 2001 From: hanna-skryl Date: Mon, 30 Mar 2026 19:06:17 -0400 Subject: [PATCH 4/4] fix(create-cli): preserve write type on repeated writes --- packages/create-cli/src/lib/setup/virtual-fs.ts | 9 +++++++-- .../create-cli/src/lib/setup/virtual-fs.unit.test.ts | 10 ++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/create-cli/src/lib/setup/virtual-fs.ts b/packages/create-cli/src/lib/setup/virtual-fs.ts index 8e8e59343..90ed531f3 100644 --- a/packages/create-cli/src/lib/setup/virtual-fs.ts +++ b/packages/create-cli/src/lib/setup/virtual-fs.ts @@ -37,8 +37,13 @@ export function createTree( }, write: async (filePath: string, content: string): Promise => { - const type = (await fs.exists(resolve(filePath))) ? 'UPDATE' : 'CREATE'; - pending.set(filePath, { content, type }); + const entry = pending.get(filePath); + if (entry) { + pending.set(filePath, { ...entry, content }); + } else { + const type = (await fs.exists(resolve(filePath))) ? 'UPDATE' : 'CREATE'; + pending.set(filePath, { content, type }); + } }, listChanges: (): FileChange[] => diff --git a/packages/create-cli/src/lib/setup/virtual-fs.unit.test.ts b/packages/create-cli/src/lib/setup/virtual-fs.unit.test.ts index 341ea6e66..031fe43f6 100644 --- a/packages/create-cli/src/lib/setup/virtual-fs.unit.test.ts +++ b/packages/create-cli/src/lib/setup/virtual-fs.unit.test.ts @@ -96,6 +96,16 @@ describe('createTree', () => { ]); }); + it('should preserve CREATE type when writing to the same path twice', async () => { + const tree = createTree('/project', createMockFs()); + await tree.write('new.ts', 'first'); + await tree.write('new.ts', 'second'); + + expect(tree.listChanges()).toStrictEqual([ + { path: 'new.ts', type: 'CREATE', content: 'second' }, + ]); + }); + it('should mark existing files as UPDATE', async () => { const tree = createTree( '/project',