From 85364013c7834f27de970cd8d317622e37510b6b Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Tue, 20 May 2025 15:19:45 +0200 Subject: [PATCH 01/10] add prompts abstraction --- code/core/src/common/index.ts | 1 + code/core/src/common/prompts/index.ts | 291 ++++++++++++++++++ code/lib/cli-storybook/src/add.ts | 6 +- .../fixes/renderer-to-framework.ts | 8 +- .../cli-storybook/src/automigrate/index.ts | 25 +- .../src/codemod/csf-factories.ts | 54 ++-- code/lib/cli-storybook/src/sandbox.ts | 22 +- 7 files changed, 336 insertions(+), 71 deletions(-) create mode 100644 code/core/src/common/prompts/index.ts diff --git a/code/core/src/common/index.ts b/code/core/src/common/index.ts index 35cbc5a51e86..d21ada8e6f28 100644 --- a/code/core/src/common/index.ts +++ b/code/core/src/common/index.ts @@ -4,6 +4,7 @@ import versions from './versions'; export * from './presets'; +export * from './prompts'; export * from './utils/cache'; export * from './utils/cli'; export * from './utils/check-addon-order'; diff --git a/code/core/src/common/prompts/index.ts b/code/core/src/common/prompts/index.ts new file mode 100644 index 000000000000..ebf33c0d02bc --- /dev/null +++ b/code/core/src/common/prompts/index.ts @@ -0,0 +1,291 @@ +import prompts from 'prompts'; + +type Option = { + value: any; + label: string; + hint?: string; +}; + +type GroupOption = { + [key: string]: Option[]; +}; + +interface BasePromptOptions { + message: string; +} + +interface TextPromptOptions extends BasePromptOptions { + placeholder?: string; + initialValue?: string; + validate?: (value: string) => string | boolean | Promise; +} + +interface ConfirmPromptOptions extends BasePromptOptions { + initialValue?: boolean; + active?: string; + inactive?: string; +} + +interface SelectPromptOptions extends BasePromptOptions { + options: Option[]; +} + +interface MultiSelectPromptOptions extends BasePromptOptions { + options: Option[]; + required?: boolean; +} + +interface GroupMultiSelectPromptOptions extends BasePromptOptions { + options: GroupOption; +} + +interface SpinnerOptions { + text?: string; +} + +interface ProgressOptions extends SpinnerOptions { + max: number; +} + +interface TaskOptions { + title: string; + task: (message: (text: string) => void) => Promise; +} + +interface PromptOptions { + onCancel?: () => void; +} + +const intro = (message: string) => { + console.log(`\n${message}\n`); +}; + +const outro = (message: string) => { + console.log(`\n${message}\n`); +}; + +const cancel = (message: string) => { + console.log(`\n❌ ${message}\n`); + process.exit(0); +}; + +const text = async (options: TextPromptOptions, promptOptions?: PromptOptions): Promise => { + const result = await prompts( + { + type: 'text', + name: 'value', + message: options.message, + initial: options.initialValue, + validate: options.validate, + }, + promptOptions + ); + + return result.value; +}; + +const confirm = async ( + options: ConfirmPromptOptions, + promptOptions?: PromptOptions +): Promise => { + const result = await prompts( + { + type: 'confirm', + name: 'value', + message: options.message, + initial: options.initialValue, + active: options.active, + inactive: options.inactive, + }, + promptOptions + ); + + return result.value; +}; + +const select = async ( + options: SelectPromptOptions, + promptOptions?: PromptOptions +): Promise => { + const result = await prompts( + { + type: 'select', + name: 'value', + message: options.message, + choices: options.options.map((opt) => ({ + title: opt.label, + value: opt.value, + description: opt.hint, + })), + }, + promptOptions + ); + + return result.value as T; +}; + +const multiselect = async ( + options: MultiSelectPromptOptions, + promptOptions?: PromptOptions +): Promise => { + const result = await prompts( + { + type: 'multiselect', + name: 'value', + message: options.message, + choices: options.options.map((opt) => ({ + title: opt.label, + value: opt.value, + description: opt.hint, + })), + }, + promptOptions + ); + + return result.value as T[]; +}; + +const groupMultiselect = async ( + options: GroupMultiSelectPromptOptions, + promptOptions?: PromptOptions +): Promise> => { + const choices = Object.entries(options.options).map(([group, opts]) => ({ + title: group, + type: 'group', + choices: opts.map((opt) => ({ + title: opt.label, + value: opt.value, + description: opt.hint, + })), + })); + + const result = await prompts( + { + type: 'multiselect', + name: 'value', + message: options.message, + choices, + }, + promptOptions + ); + + return result.value as Record; +}; + +const spinner = (options?: SpinnerOptions) => { + let currentText = options?.text || ''; + + return { + start: (text: string) => { + currentText = text; + console.log(`⏳ ${text}`); + }, + stop: (text: string) => { + console.log(`✅ ${text}`); + }, + message: (text: string) => { + console.log(` ${text}`); + }, + }; +}; + +const progress = (options: ProgressOptions) => { + const spinnerInstance = spinner(options); + let current = 0; + + return { + ...spinnerInstance, + advance: (value: number, text?: string) => { + current = value; + const percentage = Math.round((current / options.max) * 100); + spinnerInstance.message(text || `Progress: ${percentage}%`); + }, + }; +}; + +const tasks = async (tasks: TaskOptions[]) => { + for (const task of tasks) { + const spinnerInstance = spinner({ text: task.title }); + spinnerInstance.start(task.title); + + try { + const result = await task.task((text) => spinnerInstance.message(text)); + spinnerInstance.stop(result); + } catch (error: any) { + spinnerInstance.stop(`Failed: ${error.message}`); + throw error; + } + } +}; + +const log = { + info: (message: string) => console.log(`ℹ️ ${message}`), + success: (message: string) => console.log(`✅ ${message}`), + step: (message: string) => console.log(`➡️ ${message}`), + warn: (message: string) => console.log(`⚠️ ${message}`), + error: (message: string) => console.log(`❌ ${message}`), + message: (message: string, options?: { symbol?: string }) => { + const symbol = options?.symbol || '•'; + console.log(`${symbol} ${message}`); + }, +}; + +const stream = { + info: (iterable: Iterable) => { + for (const message of iterable) { + log.info(message); + } + }, + success: (iterable: Iterable) => { + for (const message of iterable) { + log.success(message); + } + }, + step: (iterable: Iterable) => { + for (const message of iterable) { + log.step(message); + } + }, + warn: (iterable: Iterable) => { + for (const message of iterable) { + log.warn(message); + } + }, + error: (iterable: Iterable) => { + for (const message of iterable) { + log.error(message); + } + }, + message: (iterable: Iterable, options?: { symbol?: string }) => { + for (const message of iterable) { + log.message(message, options); + } + }, +}; + +const taskLog = (options: { title: string }) => { + const spinnerInstance = spinner({ text: options.title }); + spinnerInstance.start(options.title); + + return { + message: (text: string) => spinnerInstance.message(text), + success: (text: string) => spinnerInstance.stop(text), + error: (text: string) => { + console.log(`❌ ${text}`); + }, + }; +}; + +export const prompt = { + confirm, + text, + select, + multiselect, + groupMultiselect, + spinner, + progress, + tasks, + log, + stream, + taskLog, +}; diff --git a/code/lib/cli-storybook/src/add.ts b/code/lib/cli-storybook/src/add.ts index b9f2a01fdfe0..72163824d612 100644 --- a/code/lib/cli-storybook/src/add.ts +++ b/code/lib/cli-storybook/src/add.ts @@ -3,6 +3,7 @@ import { isAbsolute, join } from 'node:path'; import { JsPackageManagerFactory, type PackageManagerName, + prompt, serverRequire, syncStorybookAddons, versions, @@ -10,7 +11,6 @@ import { import { readConfig, writeConfig } from 'storybook/internal/csf-tools'; import type { StorybookConfigRaw } from 'storybook/internal/types'; -import prompts from 'prompts'; import SemVer from 'semver'; import { dedent } from 'ts-dedent'; @@ -114,9 +114,7 @@ export async function add( shouldAddToMain = false; if (!yes) { logger.log(`The Storybook addon "${addonName}" is already present in ${mainConfigPath}.`); - const { shouldForceInstall } = await prompts({ - type: 'confirm', - name: 'shouldForceInstall', + const shouldForceInstall = await prompt.confirm({ message: `Do you wish to install it again?`, }); diff --git a/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.ts b/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.ts index 264e188f6b1e..4346f9c59751 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/renderer-to-framework.ts @@ -5,12 +5,12 @@ import { frameworkPackages, frameworkToRenderer, getProjectRoot, + prompt, rendererPackages, } from 'storybook/internal/common'; import type { PackageJson } from 'storybook/internal/types'; import picocolors from 'picocolors'; -import prompts from 'prompts'; import { dedent } from 'ts-dedent'; import type { Fix, RunOptions } from '../types'; @@ -183,12 +183,10 @@ export const rendererToFramework: Fix = { async run(options: RunOptions) { const { result, dryRun = false } = options; const defaultGlob = '**/*.{mjs,cjs,js,jsx,ts,tsx}'; - const { glob } = await prompts({ - type: 'text', - name: 'glob', + const glob = await prompt.text({ message: 'Enter a custom glob pattern to scan for story files (or press enter to use default):', - initial: defaultGlob, + initialValue: defaultGlob, }); const projectRoot = getProjectRoot(); diff --git a/code/lib/cli-storybook/src/automigrate/index.ts b/code/lib/cli-storybook/src/automigrate/index.ts index 9184a56eb69f..1e69b67bdeed 100644 --- a/code/lib/cli-storybook/src/automigrate/index.ts +++ b/code/lib/cli-storybook/src/automigrate/index.ts @@ -6,13 +6,13 @@ import type { PackageJson } from 'storybook/internal/common'; import { type JsPackageManager, JsPackageManagerFactory, + prompt, temporaryFile, } from 'storybook/internal/common'; import type { StorybookConfigRaw } from 'storybook/internal/types'; import boxen from 'boxen'; import picocolors from 'picocolors'; -import prompts from 'prompts'; import semver from 'semver'; import invariant from 'tiny-invariant'; import { dedent } from 'ts-dedent'; @@ -347,13 +347,11 @@ export async function runFixes({ fixSummary.manual.push(f.id); logger.info(); - const { shouldContinue } = await prompts( + const shouldContinue = await prompt.confirm( { - type: 'toggle', - name: 'shouldContinue', message: 'Select continue once you have made the required changes, or quit to exit the migration process', - initial: true, + initialValue: true, active: 'continue', inactive: 'quit', }, @@ -369,14 +367,10 @@ export async function runFixes({ break; } } else if (promptType === 'auto') { - runAnswer = await prompts( + const shouldRun = await prompt.confirm( { - type: 'confirm', - name: 'fix', - message: `Do you want to run the '${picocolors.cyan( - f.id - )}' migration on your project?`, - initial: f.promptDefaultValue ?? true, + message: `Do you want to run the '${picocolors.cyan(f.id)}' migration on your project?`, + initialValue: f.promptDefaultValue ?? true, }, { onCancel: () => { @@ -384,13 +378,11 @@ export async function runFixes({ }, } ); + runAnswer = { fix: shouldRun }; } else if (promptType === 'notification') { - runAnswer = await prompts( + const shouldContinue = await prompt.confirm( { - type: 'confirm', - name: 'fix', message: `Do you want to continue?`, - initial: true, }, { onCancel: () => { @@ -398,6 +390,7 @@ export async function runFixes({ }, } ); + runAnswer = { fix: shouldContinue }; } } catch (err) { break; diff --git a/code/lib/cli-storybook/src/codemod/csf-factories.ts b/code/lib/cli-storybook/src/codemod/csf-factories.ts index cb2913280cd4..78c58bcdc2e2 100644 --- a/code/lib/cli-storybook/src/codemod/csf-factories.ts +++ b/code/lib/cli-storybook/src/codemod/csf-factories.ts @@ -1,7 +1,6 @@ -import { type JsPackageManager, syncStorybookAddons } from 'storybook/internal/common'; +import { type JsPackageManager, prompt, syncStorybookAddons } from 'storybook/internal/common'; import picocolors from 'picocolors'; -import prompts from 'prompts'; import { dedent } from 'ts-dedent'; import { runCodemod } from '../automigrate/codemod'; @@ -24,19 +23,15 @@ async function runStoriesCodemod(options: { let globString = '{stories,src}/**/{Button,Header,Page}.stories.*'; if (!process.env.IN_STORYBOOK_SANDBOX) { logger.log('Please enter the glob for your stories to migrate'); - globString = ( - await prompts( - { - type: 'text', - name: 'glob', - message: 'glob', - initial: 'src/**/*.stories.*', - }, - { - onCancel: () => process.exit(0), - } - ) - ).glob; + globString = await prompt.text( + { + message: 'glob', + initialValue: 'src/**/*.stories.*', + }, + { + onCancel: () => process.exit(0), + } + ); } logger.log('\n🛠️ Applying codemod on your stories, this might take some time...'); @@ -89,23 +84,18 @@ export const csfFactories: CommandFix = { - ${picocolors.bold('Relative imports:')} ${picocolors.cyan("`import preview from '../../.storybook/preview'`")} `) ); - useSubPathImports = ( - await prompts( - { - type: 'select', - name: 'useSubPathImports', - message: 'Which would you like to use?', - choices: [ - { title: 'Subpath imports', value: true }, - { title: 'Relative imports', value: false }, - ], - initial: 0, - }, - { - onCancel: () => process.exit(0), - } - ) - ).useSubPathImports; + useSubPathImports = await prompt.select( + { + message: 'Which would you like to use?', + options: [ + { label: 'Subpath imports', value: true }, + { label: 'Relative imports', value: false }, + ], + }, + { + onCancel: () => process.exit(0), + } + ); } if (useSubPathImports && !packageJson.imports?.['#*']) { diff --git a/code/lib/cli-storybook/src/sandbox.ts b/code/lib/cli-storybook/src/sandbox.ts index c2d86dd9e7d6..140d8be444be 100644 --- a/code/lib/cli-storybook/src/sandbox.ts +++ b/code/lib/cli-storybook/src/sandbox.ts @@ -3,13 +3,12 @@ import { readdir, rm } from 'node:fs/promises'; import { isAbsolute, join } from 'node:path'; import type { PackageManagerName } from 'storybook/internal/common'; -import { JsPackageManagerFactory } from 'storybook/internal/common'; +import { JsPackageManagerFactory, prompt } from 'storybook/internal/common'; import { versions } from 'storybook/internal/common'; import boxen from 'boxen'; import { downloadTemplate } from 'giget'; import picocolors from 'picocolors'; -import prompts from 'prompts'; import { lt, prerelease } from 'semver'; import invariant from 'tiny-invariant'; import { dedent } from 'ts-dedent'; @@ -28,7 +27,7 @@ interface SandboxOptions { } type Choice = keyof typeof TEMPLATES; -const toChoices = (c: Choice): prompts.Choice => ({ title: TEMPLATES[c].name, value: c }); +const toChoices = (c: Choice) => ({ label: TEMPLATES[c].name, value: c }); export const sandbox = async ({ output: outputDirectory, @@ -170,12 +169,10 @@ export const sandbox = async ({ } if (!selectedDirectory) { - const { directory } = await prompts( + const directory = await prompt.text( { - type: 'text', message: 'Enter the output directory', - name: 'directory', - initial: outputDirectoryName ?? undefined, + initialValue: outputDirectoryName ?? undefined, validate: async (directoryName) => existsSync(directoryName) ? `${directoryName} already exists. Please choose another name.` @@ -272,12 +269,10 @@ export const sandbox = async ({ }; async function promptSelectedTemplate(choices: Choice[]): Promise { - const { template } = await prompts( + const selected = await prompt.select( { - type: 'select', - message: '🌈 Select the template', - name: 'template', - choices: choices.map(toChoices), + message: 'Select a template', + options: choices.map(toChoices), }, { onCancel: () => { @@ -286,6 +281,5 @@ async function promptSelectedTemplate(choices: Choice[]): Promise }, } ); - - return template || null; + return selected as Choice; } From ad924e8992d1162ecb484bafc79b7ed8d74a283c Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Tue, 20 May 2025 16:59:44 +0200 Subject: [PATCH 02/10] experiment with clack --- code/core/src/common/prompts/index.ts | 5 ++++- code/package.json | 1 + code/yarn.lock | 22 ++++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/code/core/src/common/prompts/index.ts b/code/core/src/common/prompts/index.ts index ebf33c0d02bc..1526531f667b 100644 --- a/code/core/src/common/prompts/index.ts +++ b/code/core/src/common/prompts/index.ts @@ -1,3 +1,4 @@ +import * as clack from '@clack/prompts'; import prompts from 'prompts'; type Option = { @@ -276,7 +277,9 @@ const taskLog = (options: { title: string }) => { }; }; -export const prompt = { +export const prompt = clack; + +export const prompt_ = { confirm, text, select, diff --git a/code/package.json b/code/package.json index 6a6e1db70a2a..85c13583a1f0 100644 --- a/code/package.json +++ b/code/package.json @@ -103,6 +103,7 @@ }, "dependencies": { "@chromatic-com/storybook": "^4.0.0-0", + "@clack/prompts": "^0.10.1", "@happy-dom/global-registrator": "^17.4.4", "@nx/vite": "20.2.2", "@nx/workspace": "20.2.2", diff --git a/code/yarn.lock b/code/yarn.lock index 4a3e3ca4ea12..efbbfc237fb3 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -2012,6 +2012,27 @@ __metadata: languageName: node linkType: hard +"@clack/core@npm:0.4.2": + version: 0.4.2 + resolution: "@clack/core@npm:0.4.2" + dependencies: + picocolors: "npm:^1.0.0" + sisteransi: "npm:^1.0.5" + checksum: 10c0/e4d09deb1dcbb489c4fcd9671f97863d8e1e578122da26eba5480daeb8d1959bce30dc4e03e8de5291f88e5b6e4dc22119c4d1ee0138dc8033f29708263519e7 + languageName: node + linkType: hard + +"@clack/prompts@npm:^0.10.1": + version: 0.10.1 + resolution: "@clack/prompts@npm:0.10.1" + dependencies: + "@clack/core": "npm:0.4.2" + picocolors: "npm:^1.0.0" + sisteransi: "npm:^1.0.5" + checksum: 10c0/9993564aebec8ded9b1bd5d72cd6a356c919434e99cfc8a66c65d4511011a0f96e307efd96c9fe240b83df124a8103caa211ae634ba4ccdde69e29546b64b409 + languageName: node + linkType: hard + "@colors/colors@npm:1.5.0": version: 1.5.0 resolution: "@colors/colors@npm:1.5.0" @@ -6701,6 +6722,7 @@ __metadata: resolution: "@storybook/root@workspace:." dependencies: "@chromatic-com/storybook": "npm:^4.0.0-0" + "@clack/prompts": "npm:^0.10.1" "@happy-dom/global-registrator": "npm:^17.4.4" "@nx/vite": "npm:20.2.2" "@nx/workspace": "npm:20.2.2" From a718ece51231fb29e5fdc75a06aa5a927ed3b4b6 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Tue, 20 May 2025 18:27:40 +0200 Subject: [PATCH 03/10] Encapsulate boxen usage in cli-storybook --- code/core/src/common/prompts/index.ts | 22 +++--- code/lib/cli-storybook/src/autoblock/index.ts | 18 +++-- ...ry.test.ts => logMigrationSummary.test.ts} | 67 ++++++++++++------- ...ationSummary.ts => logMigrationSummary.ts} | 7 +- .../cli-storybook/src/automigrate/index.ts | 21 +++--- .../src/codemod/csf-factories.ts | 14 ++-- code/lib/cli-storybook/src/doctor/index.ts | 17 ++--- code/lib/cli-storybook/src/sandbox.ts | 41 +++++------- code/lib/cli-storybook/src/upgrade.ts | 16 ++--- code/lib/cli-storybook/src/util.ts | 4 -- 10 files changed, 111 insertions(+), 116 deletions(-) rename code/lib/cli-storybook/src/automigrate/helpers/{getMigrationSummary.test.ts => logMigrationSummary.test.ts} (70%) rename code/lib/cli-storybook/src/automigrate/helpers/{getMigrationSummary.ts => logMigrationSummary.ts} (93%) delete mode 100644 code/lib/cli-storybook/src/util.ts diff --git a/code/core/src/common/prompts/index.ts b/code/core/src/common/prompts/index.ts index 1526531f667b..e9a87ddc0e7f 100644 --- a/code/core/src/common/prompts/index.ts +++ b/code/core/src/common/prompts/index.ts @@ -1,4 +1,5 @@ import * as clack from '@clack/prompts'; +import boxen, { type Options as BoxenOptions } from 'boxen'; import prompts from 'prompts'; type Option = { @@ -65,11 +66,6 @@ const outro = (message: string) => { console.log(`\n${message}\n`); }; -const cancel = (message: string) => { - console.log(`\n❌ ${message}\n`); - process.exit(0); -}; - const text = async (options: TextPromptOptions, promptOptions?: PromptOptions): Promise => { const result = await prompts( { @@ -125,6 +121,13 @@ const select = async ( return result.value as T; }; +const logBox = (message: string, style?: BoxenOptions) => { + console.log( + boxen(message, { borderStyle: 'round', padding: 1, borderColor: '#F1618C', ...style }) + ); +}; + +// THE UTILITIES BELOW ARE NOT USED const multiselect = async ( options: MultiSelectPromptOptions, promptOptions?: PromptOptions @@ -277,13 +280,13 @@ const taskLog = (options: { title: string }) => { }; }; -export const prompt = clack; - -export const prompt_ = { +export const prompt = { confirm, text, select, multiselect, + logBox, + // these below not really used groupMultiselect, spinner, progress, @@ -292,3 +295,6 @@ export const prompt_ = { stream, taskLog, }; + +// TODO: drop-in replacement, just rename to prompt later +export const prompt2 = clack; diff --git a/code/lib/cli-storybook/src/autoblock/index.ts b/code/lib/cli-storybook/src/autoblock/index.ts index 90c48eea9ff1..eb6a4f29ff4f 100644 --- a/code/lib/cli-storybook/src/autoblock/index.ts +++ b/code/lib/cli-storybook/src/autoblock/index.ts @@ -1,6 +1,6 @@ +import { prompt } from 'storybook/internal/common'; import { logger } from 'storybook/internal/node-logger'; -import boxen from 'boxen'; import picocolors from 'picocolors'; import type { AutoblockOptions, Blocker } from './types'; @@ -58,15 +58,13 @@ export const autoblock = async ( }; const borderColor = '#FC521F'; - logger.plain( - boxen( - [messages.welcome] - .concat(['\n\n']) - .concat([faults.map((i) => i.log).join(segmentDivider)]) - .concat([segmentDivider, messages.reminder]) - .join(''), - { borderStyle: 'round', padding: 1, borderColor } - ) + prompt.logBox( + [messages.welcome] + .concat(['\n\n']) + .concat([faults.map((i) => i.log).join(segmentDivider)]) + .concat([segmentDivider, messages.reminder]) + .join(''), + { borderStyle: 'round', padding: 1, borderColor } ); return faults[0].id; diff --git a/code/lib/cli-storybook/src/automigrate/helpers/getMigrationSummary.test.ts b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts similarity index 70% rename from code/lib/cli-storybook/src/automigrate/helpers/getMigrationSummary.test.ts rename to code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts index bff1b3dcebfe..b2f584282bbf 100644 --- a/code/lib/cli-storybook/src/automigrate/helpers/getMigrationSummary.test.ts +++ b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts @@ -1,15 +1,17 @@ import { describe, expect, it, vi } from 'vitest'; -import type { InstallationMetadata } from 'storybook/internal/common'; +import { type InstallationMetadata, prompt } from 'storybook/internal/common'; import { FixStatus } from '../types'; -import { getMigrationSummary } from './getMigrationSummary'; +import { logMigrationSummary } from './logMigrationSummary'; -vi.mock('boxen', () => ({ - default: vi.fn((str, { title = '' }) => `${title}\n\n${str.replace(/\x1b\[[0-9;]*[mG]/g, '')}`), +vi.mock('storybook/internal/common', () => ({ + prompt: { + logBox: vi.fn(), + }, })); -describe('getMigrationSummary', () => { +describe('logMigrationSummary', () => { const fixResults = { 'foo-package': FixStatus.SUCCEEDED, 'bar-package': FixStatus.MANUAL_SUCCEEDED, @@ -37,7 +39,7 @@ describe('getMigrationSummary', () => { const logFile = '/path/to/log/file'; it('renders a summary with a "no migrations" message if all migrations were unnecessary', () => { - const summary = getMigrationSummary({ + logMigrationSummary({ fixResults: { 'foo-package': FixStatus.UNNECESSARY }, fixSummary: { succeeded: [], @@ -49,11 +51,15 @@ describe('getMigrationSummary', () => { logFile, }); - expect(summary).toContain('No migrations were applicable to your project'); + expect(vi.mocked(prompt.logBox).mock.calls[0][1]).toEqual( + expect.objectContaining({ + title: 'No migrations were applicable to your project', + }) + ); }); it('renders a summary with a "check failed" message if at least one migration completely failed', () => { - const summary = getMigrationSummary({ + logMigrationSummary({ fixResults: { 'foo-package': FixStatus.SUCCEEDED, 'bar-package': FixStatus.MANUAL_SUCCEEDED, @@ -69,21 +75,28 @@ describe('getMigrationSummary', () => { logFile, }); - expect(summary).toContain('Migration check ran with failures'); + expect(vi.mocked(prompt.logBox).mock.calls[0][1]).toEqual( + expect.objectContaining({ + title: 'Migration check ran with failures', + }) + ); }); it('renders a summary with successful, manual, failed, and skipped migrations', () => { - const summary = getMigrationSummary({ + logMigrationSummary({ fixResults, fixSummary, installationMetadata: null, logFile, }); - expect(summary).toMatchInlineSnapshot(` - "Migration check ran with failures - - Successful migrations: + expect(vi.mocked(prompt.logBox).mock.calls[0][1]).toEqual( + expect.objectContaining({ + title: 'Migration check ran with failures', + }) + ); + expect(vi.mocked(prompt.logBox).mock.calls[0][0]).toMatchInlineSnapshot(` + "Successful migrations: foo-package @@ -114,17 +127,20 @@ describe('getMigrationSummary', () => { }); it('renders a summary with a warning if there are duplicated dependencies outside the allow list', () => { - const summary = getMigrationSummary({ + logMigrationSummary({ fixResults: {}, fixSummary: { succeeded: [], failed: {}, manual: [], skipped: [] }, installationMetadata, logFile, }); - expect(summary).toMatchInlineSnapshot(` - "No migrations were applicable to your project - - If you'd like to run the migrations again, you can do so by running 'npx storybook automigrate' + expect(vi.mocked(prompt.logBox).mock.calls[0][1]).toEqual( + expect.objectContaining({ + title: 'No migrations were applicable to your project', + }) + ); + expect(vi.mocked(prompt.logBox).mock.calls[0][0]).toMatchInlineSnapshot(` + "If you'd like to run the migrations again, you can do so by running 'npx storybook automigrate' The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. @@ -134,17 +150,20 @@ describe('getMigrationSummary', () => { }); it('renders a basic summary if there are no duplicated dependencies or migrations', () => { - const summary = getMigrationSummary({ + logMigrationSummary({ fixResults: {}, fixSummary: { succeeded: [], failed: {}, manual: [], skipped: [] }, installationMetadata: undefined, logFile, }); - expect(summary).toMatchInlineSnapshot(` - "No migrations were applicable to your project - - If you'd like to run the migrations again, you can do so by running 'npx storybook automigrate' + expect(vi.mocked(prompt.logBox).mock.calls[0][1]).toEqual( + expect.objectContaining({ + title: 'No migrations were applicable to your project', + }) + ); + expect(vi.mocked(prompt.logBox).mock.calls[0][0]).toMatchInlineSnapshot(` + "If you'd like to run the migrations again, you can do so by running 'npx storybook automigrate' The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. diff --git a/code/lib/cli-storybook/src/automigrate/helpers/getMigrationSummary.ts b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.ts similarity index 93% rename from code/lib/cli-storybook/src/automigrate/helpers/getMigrationSummary.ts rename to code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.ts index c8974101533d..5168d5e69999 100644 --- a/code/lib/cli-storybook/src/automigrate/helpers/getMigrationSummary.ts +++ b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.ts @@ -1,6 +1,5 @@ -import { type InstallationMetadata } from 'storybook/internal/common'; +import { type InstallationMetadata, prompt } from 'storybook/internal/common'; -import boxen from 'boxen'; import picocolors from 'picocolors'; import { dedent } from 'ts-dedent'; @@ -52,7 +51,7 @@ function getGlossaryMessages( return messages; } -export function getMigrationSummary({ +export function logMigrationSummary({ fixResults, fixSummary, logFile, @@ -89,7 +88,7 @@ export function getMigrationSummary({ ? 'Migration check ran with failures' : 'Migration check ran successfully'; - return boxen(messages.filter(Boolean).join(segmentDivider), { + return prompt.logBox(messages.filter(Boolean).join(segmentDivider), { borderStyle: 'round', padding: 1, title, diff --git a/code/lib/cli-storybook/src/automigrate/index.ts b/code/lib/cli-storybook/src/automigrate/index.ts index 1e69b67bdeed..2c4277711bee 100644 --- a/code/lib/cli-storybook/src/automigrate/index.ts +++ b/code/lib/cli-storybook/src/automigrate/index.ts @@ -11,7 +11,6 @@ import { } from 'storybook/internal/common'; import type { StorybookConfigRaw } from 'storybook/internal/types'; -import boxen from 'boxen'; import picocolors from 'picocolors'; import semver from 'semver'; import invariant from 'tiny-invariant'; @@ -30,7 +29,7 @@ import type { import { FixStatus, allFixes, commandFixes } from './fixes'; import { upgradeStorybookRelatedDependencies } from './fixes/upgrade-storybook-related-dependencies'; import { cleanLog } from './helpers/cleanLog'; -import { getMigrationSummary } from './helpers/getMigrationSummary'; +import { logMigrationSummary } from './helpers/logMigrationSummary'; import { getStorybookData } from './helpers/mainConfigFile'; const logger = console; @@ -225,9 +224,7 @@ export const automigrate = async ({ ]); logger.info(); - logger.info( - getMigrationSummary({ fixResults, fixSummary, logFile: LOG_FILE_PATH, installationMetadata }) - ); + logMigrationSummary({ fixResults, fixSummary, logFile: LOG_FILE_PATH, installationMetadata }); logger.info(); } @@ -322,14 +319,12 @@ export async function runFixes({ } }; - logger.info( - boxen(message, { - borderStyle: 'round', - padding: 1, - borderColor: '#F1618C', - title: getTitle(), - }) - ); + prompt.logBox(message, { + borderStyle: 'round', + padding: 1, + borderColor: '#F1618C', + title: getTitle(), + }); let runAnswer: { fix: boolean } | undefined; diff --git a/code/lib/cli-storybook/src/codemod/csf-factories.ts b/code/lib/cli-storybook/src/codemod/csf-factories.ts index 78c58bcdc2e2..585f3fa15b10 100644 --- a/code/lib/cli-storybook/src/codemod/csf-factories.ts +++ b/code/lib/cli-storybook/src/codemod/csf-factories.ts @@ -6,7 +6,6 @@ import { dedent } from 'ts-dedent'; import { runCodemod } from '../automigrate/codemod'; import { getFrameworkPackageName } from '../automigrate/helpers/mainConfigFile'; import type { CommandFix } from '../automigrate/types'; -import { printBoxedMessage } from '../util'; import { configToCsfFactory } from './helpers/config-to-csf-factory'; import { storyToCsfFactory } from './helpers/story-to-csf-factory'; @@ -70,8 +69,7 @@ export const csfFactories: CommandFix = { let useSubPathImports = true; if (!process.env.IN_STORYBOOK_SANDBOX) { // prompt whether the user wants to use imports map - logger.log( - printBoxedMessage(dedent` + prompt.logBox(dedent` The CSF factories format benefits from subpath imports (the imports property in your \`package.json\`), which is a node standard for module resolution. This makes it more convenient to import the preview config in your story files. However, please note that this might not work if you have an outdated tsconfig, use custom paths, or have type alias plugins configured in your project. You can always rerun this codemod and select another option to update your code later. @@ -82,8 +80,8 @@ export const csfFactories: CommandFix = { - ${picocolors.bold('Subpath imports (recommended):')} ${picocolors.cyan("`import preview from '#.storybook/preview'`")} - ${picocolors.bold('Relative imports:')} ${picocolors.cyan("`import preview from '../../.storybook/preview'`")} - `) - ); + `); + useSubPathImports = await prompt.select( { message: 'Which would you like to use?', @@ -129,15 +127,13 @@ export const csfFactories: CommandFix = { await syncStorybookAddons(mainConfig, previewConfigPath!); - logger.log( - printBoxedMessage( - dedent` + prompt.logBox( + dedent` You can now run Storybook with the new CSF factories format. For more info, check out the docs: ${picocolors.yellow('https://storybook.js.org/docs/api/csf/csf-factories')} ` - ) ); }, }; diff --git a/code/lib/cli-storybook/src/doctor/index.ts b/code/lib/cli-storybook/src/doctor/index.ts index 1c7a03477c80..561ed696fdf9 100644 --- a/code/lib/cli-storybook/src/doctor/index.ts +++ b/code/lib/cli-storybook/src/doctor/index.ts @@ -2,10 +2,9 @@ import { createWriteStream } from 'node:fs'; import { rename, rm } from 'node:fs/promises'; import { join } from 'node:path'; -import { JsPackageManagerFactory, temporaryFile } from 'storybook/internal/common'; +import { JsPackageManagerFactory, prompt, temporaryFile } from 'storybook/internal/common'; import type { PackageManagerName } from 'storybook/internal/common'; -import boxen from 'boxen'; import picocolors from 'picocolors'; import { dedent } from 'ts-dedent'; @@ -58,14 +57,12 @@ export const doctor = async ({ let foundIssues = false; const logDiagnostic = (title: string, message: string) => { foundIssues = true; - logger.info( - boxen(message, { - borderStyle: 'round', - padding: 1, - title, - borderColor: '#F1618C', - }) - ); + prompt.logBox(message, { + borderStyle: 'round', + padding: 1, + title, + borderColor: '#F1618C', + }); }; logger.info('🩺 The doctor is checking the health of your Storybook..'); diff --git a/code/lib/cli-storybook/src/sandbox.ts b/code/lib/cli-storybook/src/sandbox.ts index 140d8be444be..e135514aa6a4 100644 --- a/code/lib/cli-storybook/src/sandbox.ts +++ b/code/lib/cli-storybook/src/sandbox.ts @@ -6,7 +6,6 @@ import type { PackageManagerName } from 'storybook/internal/common'; import { JsPackageManagerFactory, prompt } from 'storybook/internal/common'; import { versions } from 'storybook/internal/common'; -import boxen from 'boxen'; import { downloadTemplate } from 'giget'; import picocolors from 'picocolors'; import { lt, prerelease } from 'semver'; @@ -69,15 +68,13 @@ export const sandbox = async ({ prerelease: picocolors.yellow('This is a pre-release version.'), }; - logger.log( - boxen( - [messages.welcome] - .concat(isOutdated && !isPrerelease ? [messages.notLatest] : []) - .concat(init && (isOutdated || isPrerelease) ? [messages.longInitTime] : []) - .concat(isPrerelease ? [messages.prerelease] : []) - .join('\n'), - { borderStyle: 'round', padding: 1, borderColor } - ) + prompt.logBox( + [messages.welcome] + .concat(isOutdated && !isPrerelease ? [messages.notLatest] : []) + .concat(init && (isOutdated || isPrerelease) ? [messages.longInitTime] : []) + .concat(isPrerelease ? [messages.prerelease] : []) + .join('\n'), + { borderStyle: 'round', padding: 1, borderColor } ); if (!selectedConfig) { @@ -108,9 +105,8 @@ export const sandbox = async ({ }, []); if (choices.length === 0) { - logger.info( - boxen( - dedent` + prompt.logBox( + dedent` 🔎 You filtered out all templates. 🔍 After filtering all the templates with "${picocolors.yellow( @@ -120,8 +116,7 @@ export const sandbox = async ({ Available templates: ${keys.map((key) => picocolors.blue(`- ${key}`)).join('\n')} `.trim(), - { borderStyle: 'round', padding: 1, borderColor: '#F1618C' } as any - ) + { borderStyle: 'round', padding: 1, borderColor: '#F1618C' } as any ); process.exit(1); } @@ -129,9 +124,8 @@ export const sandbox = async ({ if (choices.length === 1) { [templateId] = choices; } else { - logger.info( - boxen( - dedent` + prompt.logBox( + dedent` 🤗 Welcome to ${picocolors.yellow('sb sandbox')}! 🤗 Create a ${picocolors.green('new project')} to minimally reproduce Storybook issues. @@ -141,8 +135,7 @@ export const sandbox = async ({ After the reproduction is ready, we'll guide you through the next steps. `.trim(), - { borderStyle: 'round', padding: 1, borderColor: '#F1618C' } as any - ) + { borderStyle: 'round', padding: 1, borderColor: '#F1618C' } as any ); templateId = await promptSelectedTemplate(choices); @@ -243,9 +236,8 @@ export const sandbox = async ({ `) : `Recreate your setup, then ${picocolors.yellow(`npx storybook@latest init`)}`; - logger.info( - boxen( - dedent` + prompt.logBox( + dedent` 🎉 Your Storybook reproduction project is ready to use! 🎉 ${picocolors.yellow(`cd ${selectedDirectory}`)} @@ -259,8 +251,7 @@ export const sandbox = async ({ Having a clean repro helps us solve your issue faster! 🙏 `.trim(), - { borderStyle: 'round', padding: 1, borderColor: '#F1618C' } as any - ) + { borderStyle: 'round', padding: 1, borderColor: '#F1618C' } ); } catch (error) { logger.error('🚨 Failed to create sandbox'); diff --git a/code/lib/cli-storybook/src/upgrade.ts b/code/lib/cli-storybook/src/upgrade.ts index 6b11e0b6e211..37be1bc4d120 100644 --- a/code/lib/cli-storybook/src/upgrade.ts +++ b/code/lib/cli-storybook/src/upgrade.ts @@ -4,6 +4,7 @@ import { JsPackageManagerFactory, isCorePackage, isSatelliteAddon, + prompt, versions, } from 'storybook/internal/common'; import { withTelemetry } from 'storybook/internal/core-server'; @@ -16,7 +17,6 @@ import { } from 'storybook/internal/server-errors'; import { telemetry } from 'storybook/internal/telemetry'; -import boxen from 'boxen'; import { sync as spawnSync } from 'cross-spawn'; import picocolors from 'picocolors'; import semver, { clean, eq, lt, prerelease } from 'semver'; @@ -276,14 +276,12 @@ export const doUpgrade = async (allOptions: UpgradeOptions) => { prerelease: picocolors.yellow('This is a pre-release version.'), }; - logger.plain( - boxen( - [messages.welcome] - .concat(isCLIOutdated && !isCLIPrerelease ? [messages.notLatest] : []) - .concat(isCLIPrerelease ? [messages.prerelease] : []) - .join('\n'), - { borderStyle: 'round', padding: 1, borderColor } - ) + prompt.logBox( + [messages.welcome] + .concat(isCLIOutdated && !isCLIPrerelease ? [messages.notLatest] : []) + .concat(isCLIPrerelease ? [messages.prerelease] : []) + .join('\n'), + { borderStyle: 'round', padding: 1, borderColor } ); let results; diff --git a/code/lib/cli-storybook/src/util.ts b/code/lib/cli-storybook/src/util.ts deleted file mode 100644 index f8fa3f3d6f3a..000000000000 --- a/code/lib/cli-storybook/src/util.ts +++ /dev/null @@ -1,4 +0,0 @@ -import boxen, { type Options } from 'boxen'; - -export const printBoxedMessage = (message: string, style?: Options) => - boxen(message, { borderStyle: 'round', padding: 1, borderColor: '#F1618C', ...style }); From 745682d49ab27d8dca93a32b73b8fc3b7ad77aa0 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Tue, 20 May 2025 19:11:22 +0200 Subject: [PATCH 04/10] cleanup --- .../src/automigrate/helpers/logMigrationSummary.ts | 2 -- code/lib/cli-storybook/src/automigrate/index.ts | 3 --- code/lib/cli-storybook/src/doctor/index.ts | 3 --- 3 files changed, 8 deletions(-) diff --git a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.ts b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.ts index 5168d5e69999..c24533b017de 100644 --- a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.ts +++ b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.ts @@ -89,8 +89,6 @@ export function logMigrationSummary({ : 'Migration check ran successfully'; return prompt.logBox(messages.filter(Boolean).join(segmentDivider), { - borderStyle: 'round', - padding: 1, title, borderColor: hasFailures ? 'red' : 'green', }); diff --git a/code/lib/cli-storybook/src/automigrate/index.ts b/code/lib/cli-storybook/src/automigrate/index.ts index 2c4277711bee..e3015a115c0d 100644 --- a/code/lib/cli-storybook/src/automigrate/index.ts +++ b/code/lib/cli-storybook/src/automigrate/index.ts @@ -320,9 +320,6 @@ export async function runFixes({ }; prompt.logBox(message, { - borderStyle: 'round', - padding: 1, - borderColor: '#F1618C', title: getTitle(), }); diff --git a/code/lib/cli-storybook/src/doctor/index.ts b/code/lib/cli-storybook/src/doctor/index.ts index 561ed696fdf9..fdcd909fbbcf 100644 --- a/code/lib/cli-storybook/src/doctor/index.ts +++ b/code/lib/cli-storybook/src/doctor/index.ts @@ -58,10 +58,7 @@ export const doctor = async ({ const logDiagnostic = (title: string, message: string) => { foundIssues = true; prompt.logBox(message, { - borderStyle: 'round', - padding: 1, title, - borderColor: '#F1618C', }); }; From 16531c5bb6b3575f01a7627375dcbb5842be9b80 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Wed, 21 May 2025 16:19:54 +0200 Subject: [PATCH 05/10] cleanup --- code/core/src/common/prompts/index.ts | 200 -------------------------- code/package.json | 1 - code/yarn.lock | 22 --- 3 files changed, 223 deletions(-) diff --git a/code/core/src/common/prompts/index.ts b/code/core/src/common/prompts/index.ts index e9a87ddc0e7f..19235be53d8d 100644 --- a/code/core/src/common/prompts/index.ts +++ b/code/core/src/common/prompts/index.ts @@ -1,4 +1,3 @@ -import * as clack from '@clack/prompts'; import boxen, { type Options as BoxenOptions } from 'boxen'; import prompts from 'prompts'; @@ -8,10 +7,6 @@ type Option = { hint?: string; }; -type GroupOption = { - [key: string]: Option[]; -}; - interface BasePromptOptions { message: string; } @@ -32,40 +27,10 @@ interface SelectPromptOptions extends BasePromptOptions { options: Option[]; } -interface MultiSelectPromptOptions extends BasePromptOptions { - options: Option[]; - required?: boolean; -} - -interface GroupMultiSelectPromptOptions extends BasePromptOptions { - options: GroupOption; -} - -interface SpinnerOptions { - text?: string; -} - -interface ProgressOptions extends SpinnerOptions { - max: number; -} - -interface TaskOptions { - title: string; - task: (message: (text: string) => void) => Promise; -} - interface PromptOptions { onCancel?: () => void; } -const intro = (message: string) => { - console.log(`\n${message}\n`); -}; - -const outro = (message: string) => { - console.log(`\n${message}\n`); -}; - const text = async (options: TextPromptOptions, promptOptions?: PromptOptions): Promise => { const result = await prompts( { @@ -127,174 +92,9 @@ const logBox = (message: string, style?: BoxenOptions) => { ); }; -// THE UTILITIES BELOW ARE NOT USED -const multiselect = async ( - options: MultiSelectPromptOptions, - promptOptions?: PromptOptions -): Promise => { - const result = await prompts( - { - type: 'multiselect', - name: 'value', - message: options.message, - choices: options.options.map((opt) => ({ - title: opt.label, - value: opt.value, - description: opt.hint, - })), - }, - promptOptions - ); - - return result.value as T[]; -}; - -const groupMultiselect = async ( - options: GroupMultiSelectPromptOptions, - promptOptions?: PromptOptions -): Promise> => { - const choices = Object.entries(options.options).map(([group, opts]) => ({ - title: group, - type: 'group', - choices: opts.map((opt) => ({ - title: opt.label, - value: opt.value, - description: opt.hint, - })), - })); - - const result = await prompts( - { - type: 'multiselect', - name: 'value', - message: options.message, - choices, - }, - promptOptions - ); - - return result.value as Record; -}; - -const spinner = (options?: SpinnerOptions) => { - let currentText = options?.text || ''; - - return { - start: (text: string) => { - currentText = text; - console.log(`⏳ ${text}`); - }, - stop: (text: string) => { - console.log(`✅ ${text}`); - }, - message: (text: string) => { - console.log(` ${text}`); - }, - }; -}; - -const progress = (options: ProgressOptions) => { - const spinnerInstance = spinner(options); - let current = 0; - - return { - ...spinnerInstance, - advance: (value: number, text?: string) => { - current = value; - const percentage = Math.round((current / options.max) * 100); - spinnerInstance.message(text || `Progress: ${percentage}%`); - }, - }; -}; - -const tasks = async (tasks: TaskOptions[]) => { - for (const task of tasks) { - const spinnerInstance = spinner({ text: task.title }); - spinnerInstance.start(task.title); - - try { - const result = await task.task((text) => spinnerInstance.message(text)); - spinnerInstance.stop(result); - } catch (error: any) { - spinnerInstance.stop(`Failed: ${error.message}`); - throw error; - } - } -}; - -const log = { - info: (message: string) => console.log(`ℹ️ ${message}`), - success: (message: string) => console.log(`✅ ${message}`), - step: (message: string) => console.log(`➡️ ${message}`), - warn: (message: string) => console.log(`⚠️ ${message}`), - error: (message: string) => console.log(`❌ ${message}`), - message: (message: string, options?: { symbol?: string }) => { - const symbol = options?.symbol || '•'; - console.log(`${symbol} ${message}`); - }, -}; - -const stream = { - info: (iterable: Iterable) => { - for (const message of iterable) { - log.info(message); - } - }, - success: (iterable: Iterable) => { - for (const message of iterable) { - log.success(message); - } - }, - step: (iterable: Iterable) => { - for (const message of iterable) { - log.step(message); - } - }, - warn: (iterable: Iterable) => { - for (const message of iterable) { - log.warn(message); - } - }, - error: (iterable: Iterable) => { - for (const message of iterable) { - log.error(message); - } - }, - message: (iterable: Iterable, options?: { symbol?: string }) => { - for (const message of iterable) { - log.message(message, options); - } - }, -}; - -const taskLog = (options: { title: string }) => { - const spinnerInstance = spinner({ text: options.title }); - spinnerInstance.start(options.title); - - return { - message: (text: string) => spinnerInstance.message(text), - success: (text: string) => spinnerInstance.stop(text), - error: (text: string) => { - console.log(`❌ ${text}`); - }, - }; -}; - export const prompt = { confirm, text, select, - multiselect, logBox, - // these below not really used - groupMultiselect, - spinner, - progress, - tasks, - log, - stream, - taskLog, }; - -// TODO: drop-in replacement, just rename to prompt later -export const prompt2 = clack; diff --git a/code/package.json b/code/package.json index 85c13583a1f0..6a6e1db70a2a 100644 --- a/code/package.json +++ b/code/package.json @@ -103,7 +103,6 @@ }, "dependencies": { "@chromatic-com/storybook": "^4.0.0-0", - "@clack/prompts": "^0.10.1", "@happy-dom/global-registrator": "^17.4.4", "@nx/vite": "20.2.2", "@nx/workspace": "20.2.2", diff --git a/code/yarn.lock b/code/yarn.lock index efbbfc237fb3..4a3e3ca4ea12 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -2012,27 +2012,6 @@ __metadata: languageName: node linkType: hard -"@clack/core@npm:0.4.2": - version: 0.4.2 - resolution: "@clack/core@npm:0.4.2" - dependencies: - picocolors: "npm:^1.0.0" - sisteransi: "npm:^1.0.5" - checksum: 10c0/e4d09deb1dcbb489c4fcd9671f97863d8e1e578122da26eba5480daeb8d1959bce30dc4e03e8de5291f88e5b6e4dc22119c4d1ee0138dc8033f29708263519e7 - languageName: node - linkType: hard - -"@clack/prompts@npm:^0.10.1": - version: 0.10.1 - resolution: "@clack/prompts@npm:0.10.1" - dependencies: - "@clack/core": "npm:0.4.2" - picocolors: "npm:^1.0.0" - sisteransi: "npm:^1.0.5" - checksum: 10c0/9993564aebec8ded9b1bd5d72cd6a356c919434e99cfc8a66c65d4511011a0f96e307efd96c9fe240b83df124a8103caa211ae634ba4ccdde69e29546b64b409 - languageName: node - linkType: hard - "@colors/colors@npm:1.5.0": version: 1.5.0 resolution: "@colors/colors@npm:1.5.0" @@ -6722,7 +6701,6 @@ __metadata: resolution: "@storybook/root@workspace:." dependencies: "@chromatic-com/storybook": "npm:^4.0.0-0" - "@clack/prompts": "npm:^0.10.1" "@happy-dom/global-registrator": "npm:^17.4.4" "@nx/vite": "npm:20.2.2" "@nx/workspace": "npm:20.2.2" From b7df2845d00c04e73878e76e7ba01a3f1549550f Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Wed, 21 May 2025 16:38:01 +0200 Subject: [PATCH 06/10] fix types --- code/core/src/common/prompts/index.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/code/core/src/common/prompts/index.ts b/code/core/src/common/prompts/index.ts index 19235be53d8d..ca55a4d2a1f5 100644 --- a/code/core/src/common/prompts/index.ts +++ b/code/core/src/common/prompts/index.ts @@ -1,4 +1,4 @@ -import boxen, { type Options as BoxenOptions } from 'boxen'; +import boxen from 'boxen'; import prompts from 'prompts'; type Option = { @@ -86,6 +86,15 @@ const select = async ( return result.value as T; }; +type BoxenOptions = { + borderStyle?: 'round' | 'none'; + padding?: number; + title?: string; + titleAlignment?: 'left' | 'center' | 'right'; + borderColor?: string; + backgroundColor?: string; +}; + const logBox = (message: string, style?: BoxenOptions) => { console.log( boxen(message, { borderStyle: 'round', padding: 1, borderColor: '#F1618C', ...style }) From 79912030c556c6858f172fe43a8a152c0f6555a6 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Thu, 22 May 2025 11:09:04 +0200 Subject: [PATCH 07/10] use onCancel as a base option --- code/core/src/common/prompts/index.ts | 10 ++++-- .../src/codemod/csf-factories.ts | 32 +++++++------------ code/lib/cli-storybook/src/sandbox.ts | 16 +++------- 3 files changed, 22 insertions(+), 36 deletions(-) diff --git a/code/core/src/common/prompts/index.ts b/code/core/src/common/prompts/index.ts index ca55a4d2a1f5..864f3f620853 100644 --- a/code/core/src/common/prompts/index.ts +++ b/code/core/src/common/prompts/index.ts @@ -31,6 +31,10 @@ interface PromptOptions { onCancel?: () => void; } +const baseOptions: PromptOptions = { + onCancel: () => process.exit(0), +}; + const text = async (options: TextPromptOptions, promptOptions?: PromptOptions): Promise => { const result = await prompts( { @@ -40,7 +44,7 @@ const text = async (options: TextPromptOptions, promptOptions?: PromptOptions): initial: options.initialValue, validate: options.validate, }, - promptOptions + { ...baseOptions, ...promptOptions } ); return result.value; @@ -59,7 +63,7 @@ const confirm = async ( active: options.active, inactive: options.inactive, }, - promptOptions + { ...baseOptions, ...promptOptions } ); return result.value; @@ -80,7 +84,7 @@ const select = async ( description: opt.hint, })), }, - promptOptions + { ...baseOptions, ...promptOptions } ); return result.value as T; diff --git a/code/lib/cli-storybook/src/codemod/csf-factories.ts b/code/lib/cli-storybook/src/codemod/csf-factories.ts index 585f3fa15b10..f72d50a107cd 100644 --- a/code/lib/cli-storybook/src/codemod/csf-factories.ts +++ b/code/lib/cli-storybook/src/codemod/csf-factories.ts @@ -22,15 +22,10 @@ async function runStoriesCodemod(options: { let globString = '{stories,src}/**/{Button,Header,Page}.stories.*'; if (!process.env.IN_STORYBOOK_SANDBOX) { logger.log('Please enter the glob for your stories to migrate'); - globString = await prompt.text( - { - message: 'glob', - initialValue: 'src/**/*.stories.*', - }, - { - onCancel: () => process.exit(0), - } - ); + globString = await prompt.text({ + message: 'glob', + initialValue: 'src/**/*.stories.*', + }); } logger.log('\n🛠️ Applying codemod on your stories, this might take some time...'); @@ -82,18 +77,13 @@ export const csfFactories: CommandFix = { - ${picocolors.bold('Relative imports:')} ${picocolors.cyan("`import preview from '../../.storybook/preview'`")} `); - useSubPathImports = await prompt.select( - { - message: 'Which would you like to use?', - options: [ - { label: 'Subpath imports', value: true }, - { label: 'Relative imports', value: false }, - ], - }, - { - onCancel: () => process.exit(0), - } - ); + useSubPathImports = await prompt.select({ + message: 'Which would you like to use?', + options: [ + { label: 'Subpath imports', value: true }, + { label: 'Relative imports', value: false }, + ], + }); } if (useSubPathImports && !packageJson.imports?.['#*']) { diff --git a/code/lib/cli-storybook/src/sandbox.ts b/code/lib/cli-storybook/src/sandbox.ts index e135514aa6a4..e40d6129dfc0 100644 --- a/code/lib/cli-storybook/src/sandbox.ts +++ b/code/lib/cli-storybook/src/sandbox.ts @@ -260,17 +260,9 @@ export const sandbox = async ({ }; async function promptSelectedTemplate(choices: Choice[]): Promise { - const selected = await prompt.select( - { - message: 'Select a template', - options: choices.map(toChoices), - }, - { - onCancel: () => { - logger.log('Command cancelled by the user. Exiting...'); - process.exit(1); - }, - } - ); + const selected = await prompt.select({ + message: 'Select a template', + options: choices.map(toChoices), + }); return selected as Choice; } From 50e1bd2d8b5f5110628737a71de953e298d481bb Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Thu, 22 May 2025 11:38:09 +0200 Subject: [PATCH 08/10] fix tests --- .../cli-storybook/src/autoblock/index.test.ts | 14 +++++++---- .../helpers/logMigrationSummary.test.ts | 23 +++++++++++-------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/code/lib/cli-storybook/src/autoblock/index.test.ts b/code/lib/cli-storybook/src/autoblock/index.test.ts index ad1997b355e5..87030dba6599 100644 --- a/code/lib/cli-storybook/src/autoblock/index.test.ts +++ b/code/lib/cli-storybook/src/autoblock/index.test.ts @@ -2,7 +2,7 @@ import { stripVTControlCharacters } from 'node:util'; import { expect, test, vi } from 'vitest'; -import { JsPackageManagerFactory } from 'storybook/internal/common'; +import { JsPackageManagerFactory, prompt as promptRaw } from 'storybook/internal/common'; import { logger as loggerRaw } from 'storybook/internal/node-logger'; import { autoblock } from './index'; @@ -12,8 +12,11 @@ vi.mock('node:fs/promises', async (importOriginal) => ({ ...(await importOriginal()), writeFile: vi.fn(), })); -vi.mock('boxen', () => ({ - default: vi.fn((x) => x), +vi.mock('storybook/internal/common', async (importOriginal) => ({ + ...(await importOriginal()), + prompt: { + logBox: vi.fn((x) => x), + }, })); vi.mock('storybook/internal/node-logger', () => ({ logger: { @@ -24,6 +27,7 @@ vi.mock('storybook/internal/node-logger', () => ({ })); const logger = vi.mocked(loggerRaw); +const prompt = vi.mocked(promptRaw); const blockers = { alwaysPass: createBlocker({ @@ -78,7 +82,7 @@ test('1 fail', async () => { ]); expect(result).toBe('alwaysFail'); - expect(stripVTControlCharacters(logger.plain.mock.calls[0][0])).toMatchInlineSnapshot(` + expect(stripVTControlCharacters(prompt.logBox.mock.calls[0][0])).toMatchInlineSnapshot(` "Storybook has found potential blockers in your project that need to be resolved before upgrading: Always fail @@ -95,7 +99,7 @@ test('multiple fails', async () => { Promise.resolve({ blocker: blockers.alwaysFail }), Promise.resolve({ blocker: blockers.alwaysFail2 }), ]); - expect(stripVTControlCharacters(logger.plain.mock.calls[0][0])).toMatchInlineSnapshot(` + expect(stripVTControlCharacters(prompt.logBox.mock.calls[0][0])).toMatchInlineSnapshot(` "Storybook has found potential blockers in your project that need to be resolved before upgrading: Always fail diff --git a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts index b2f584282bbf..0b037ecc2f42 100644 --- a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts +++ b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts @@ -1,16 +1,19 @@ import { describe, expect, it, vi } from 'vitest'; -import { type InstallationMetadata, prompt } from 'storybook/internal/common'; +import { type InstallationMetadata, prompt as promptRaw } from 'storybook/internal/common'; import { FixStatus } from '../types'; +import { cleanLog } from './cleanLog'; import { logMigrationSummary } from './logMigrationSummary'; vi.mock('storybook/internal/common', () => ({ prompt: { - logBox: vi.fn(), + logBox: vi.fn((x) => cleanLog(x)), }, })); +const prompt = vi.mocked(promptRaw); + describe('logMigrationSummary', () => { const fixResults = { 'foo-package': FixStatus.SUCCEEDED, @@ -51,7 +54,7 @@ describe('logMigrationSummary', () => { logFile, }); - expect(vi.mocked(prompt.logBox).mock.calls[0][1]).toEqual( + expect(prompt.logBox.mock.calls[0][1]).toEqual( expect.objectContaining({ title: 'No migrations were applicable to your project', }) @@ -75,7 +78,7 @@ describe('logMigrationSummary', () => { logFile, }); - expect(vi.mocked(prompt.logBox).mock.calls[0][1]).toEqual( + expect(prompt.logBox.mock.calls[0][1]).toEqual( expect.objectContaining({ title: 'Migration check ran with failures', }) @@ -90,12 +93,12 @@ describe('logMigrationSummary', () => { logFile, }); - expect(vi.mocked(prompt.logBox).mock.calls[0][1]).toEqual( + expect(prompt.logBox.mock.calls[0][1]).toEqual( expect.objectContaining({ title: 'Migration check ran with failures', }) ); - expect(vi.mocked(prompt.logBox).mock.calls[0][0]).toMatchInlineSnapshot(` + expect(prompt.logBox.mock.calls[0][0]).toMatchInlineSnapshot(` "Successful migrations: foo-package @@ -134,12 +137,12 @@ describe('logMigrationSummary', () => { logFile, }); - expect(vi.mocked(prompt.logBox).mock.calls[0][1]).toEqual( + expect(prompt.logBox.mock.calls[0][1]).toEqual( expect.objectContaining({ title: 'No migrations were applicable to your project', }) ); - expect(vi.mocked(prompt.logBox).mock.calls[0][0]).toMatchInlineSnapshot(` + expect(prompt.logBox.mock.calls[0][0]).toMatchInlineSnapshot(` "If you'd like to run the migrations again, you can do so by running 'npx storybook automigrate' The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. @@ -157,12 +160,12 @@ describe('logMigrationSummary', () => { logFile, }); - expect(vi.mocked(prompt.logBox).mock.calls[0][1]).toEqual( + expect(prompt.logBox.mock.calls[0][1]).toEqual( expect.objectContaining({ title: 'No migrations were applicable to your project', }) ); - expect(vi.mocked(prompt.logBox).mock.calls[0][0]).toMatchInlineSnapshot(` + expect(prompt.logBox.mock.calls[0][0]).toMatchInlineSnapshot(` "If you'd like to run the migrations again, you can do so by running 'npx storybook automigrate' The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. From 6cee77ad6fe5d9cd8ee03edde60179fdbd55b3c2 Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Thu, 22 May 2025 12:08:03 +0200 Subject: [PATCH 09/10] try to fix test on windows --- .../src/automigrate/helpers/logMigrationSummary.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts index 0b037ecc2f42..cc74286a819a 100644 --- a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts +++ b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts @@ -14,6 +14,9 @@ vi.mock('storybook/internal/common', () => ({ const prompt = vi.mocked(promptRaw); +// necessary for windows and unix output to match in the assertions +const normalizeLineBreaks = (str: string) => str.replace(/\r/g, '').trim(); + describe('logMigrationSummary', () => { const fixResults = { 'foo-package': FixStatus.SUCCEEDED, @@ -98,7 +101,7 @@ describe('logMigrationSummary', () => { title: 'Migration check ran with failures', }) ); - expect(prompt.logBox.mock.calls[0][0]).toMatchInlineSnapshot(` + expect(normalizeLineBreaks(prompt.logBox.mock.calls[0][0])).toMatchInlineSnapshot(` "Successful migrations: foo-package @@ -142,7 +145,7 @@ describe('logMigrationSummary', () => { title: 'No migrations were applicable to your project', }) ); - expect(prompt.logBox.mock.calls[0][0]).toMatchInlineSnapshot(` + expect(normalizeLineBreaks(prompt.logBox.mock.calls[0][0])).toMatchInlineSnapshot(` "If you'd like to run the migrations again, you can do so by running 'npx storybook automigrate' The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. @@ -165,7 +168,7 @@ describe('logMigrationSummary', () => { title: 'No migrations were applicable to your project', }) ); - expect(prompt.logBox.mock.calls[0][0]).toMatchInlineSnapshot(` + expect(normalizeLineBreaks(prompt.logBox.mock.calls[0][0])).toMatchInlineSnapshot(` "If you'd like to run the migrations again, you can do so by running 'npx storybook automigrate' The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. From 3a55010d6c1db4b4552d5f47e9a0c15788d1eafd Mon Sep 17 00:00:00 2001 From: Yann Braga Date: Thu, 22 May 2025 13:12:01 +0200 Subject: [PATCH 10/10] fix --- .../helpers/logMigrationSummary.test.ts | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts index cc74286a819a..2cc350ffe61d 100644 --- a/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts +++ b/code/lib/cli-storybook/src/automigrate/helpers/logMigrationSummary.test.ts @@ -3,19 +3,19 @@ import { describe, expect, it, vi } from 'vitest'; import { type InstallationMetadata, prompt as promptRaw } from 'storybook/internal/common'; import { FixStatus } from '../types'; -import { cleanLog } from './cleanLog'; import { logMigrationSummary } from './logMigrationSummary'; +vi.mock('picocolors'); vi.mock('storybook/internal/common', () => ({ prompt: { - logBox: vi.fn((x) => cleanLog(x)), + logBox: vi.fn(), }, })); const prompt = vi.mocked(promptRaw); // necessary for windows and unix output to match in the assertions -const normalizeLineBreaks = (str: string) => str.replace(/\r/g, '').trim(); +const normalizeLineBreaks = (str: string) => str.replace(/\r\n|\r|\n/g, '\n').trim(); describe('logMigrationSummary', () => { const fixResults = { @@ -102,33 +102,27 @@ describe('logMigrationSummary', () => { }) ); expect(normalizeLineBreaks(prompt.logBox.mock.calls[0][0])).toMatchInlineSnapshot(` - "Successful migrations: + "undefined: + Some error message + + You can find the full logs in undefined - foo-package - Failed migrations: - baz-package: - Some error message - You can find the full logs in /path/to/log/file - Manual migrations: - bar-package - Skipped migrations: - quux-package ───────────────────────────────────────────────── - If you'd like to run the migrations again, you can do so by running 'npx storybook automigrate' + If you'd like to run the migrations again, you can do so by running 'undefined' The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. - Please check the changelog and migration guide for manual migrations and more information: https://storybook.js.org/docs/migration-guide - And reach out on Discord if you need help: https://discord.gg/storybook" + Please check the changelog and migration guide for manual migrations and more information: undefined + And reach out on Discord if you need help: undefined" `); }); @@ -146,12 +140,12 @@ describe('logMigrationSummary', () => { }) ); expect(normalizeLineBreaks(prompt.logBox.mock.calls[0][0])).toMatchInlineSnapshot(` - "If you'd like to run the migrations again, you can do so by running 'npx storybook automigrate' + "If you'd like to run the migrations again, you can do so by running 'undefined' The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. - Please check the changelog and migration guide for manual migrations and more information: https://storybook.js.org/docs/migration-guide - And reach out on Discord if you need help: https://discord.gg/storybook" + Please check the changelog and migration guide for manual migrations and more information: undefined + And reach out on Discord if you need help: undefined" `); }); @@ -169,12 +163,12 @@ describe('logMigrationSummary', () => { }) ); expect(normalizeLineBreaks(prompt.logBox.mock.calls[0][0])).toMatchInlineSnapshot(` - "If you'd like to run the migrations again, you can do so by running 'npx storybook automigrate' + "If you'd like to run the migrations again, you can do so by running 'undefined' The automigrations try to migrate common patterns in your project, but might not contain everything needed to migrate to the latest version of Storybook. - Please check the changelog and migration guide for manual migrations and more information: https://storybook.js.org/docs/migration-guide - And reach out on Discord if you need help: https://discord.gg/storybook" + Please check the changelog and migration guide for manual migrations and more information: undefined + And reach out on Discord if you need help: undefined" `); }); });