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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions code/core/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
113 changes: 113 additions & 0 deletions code/core/src/common/prompts/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import boxen from 'boxen';
import prompts from 'prompts';

type Option = {
value: any;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Using any type reduces type safety. Consider using a generic type parameter for value to maintain type checking

Suggested change
value: any;
value: T;

label: string;
hint?: string;
};

interface BasePromptOptions {
message: string;
}

interface TextPromptOptions extends BasePromptOptions {
placeholder?: string;
initialValue?: string;
validate?: (value: string) => string | boolean | Promise<string | boolean>;
}

interface ConfirmPromptOptions extends BasePromptOptions {
initialValue?: boolean;
active?: string;
inactive?: string;
}

interface SelectPromptOptions extends BasePromptOptions {
options: Option[];
}

interface PromptOptions {
onCancel?: () => void;
}

const baseOptions: PromptOptions = {
onCancel: () => process.exit(0),
};
Comment on lines +34 to +36
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Calling process.exit(0) directly in onCancel may prevent cleanup code from running. Consider allowing custom cancel handlers to be passed in


const text = async (options: TextPromptOptions, promptOptions?: PromptOptions): Promise<string> => {
const result = await prompts(
{
type: 'text',
name: 'value',
message: options.message,
initial: options.initialValue,
validate: options.validate,
},
{ ...baseOptions, ...promptOptions }
);

return result.value;
};
Comment on lines +50 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: No error handling if result.value is undefined (e.g. if user cancels). Should handle this case explicitly

Suggested change
return result.value;
};
if (result.value === undefined) {
throw new Error('Prompt was cancelled');
}
return result.value;
};


const confirm = async (
options: ConfirmPromptOptions,
promptOptions?: PromptOptions
): Promise<boolean> => {
const result = await prompts(
{
type: 'confirm',
name: 'value',
message: options.message,
initial: options.initialValue,
active: options.active,
inactive: options.inactive,
},
{ ...baseOptions, ...promptOptions }
);

return result.value;
};

const select = async <T>(
options: SelectPromptOptions,
promptOptions?: PromptOptions
): Promise<T> => {
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,
})),
},
{ ...baseOptions, ...promptOptions }
);

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 })
);
};

export const prompt = {
confirm,
text,
select,
logBox,
};
6 changes: 2 additions & 4 deletions code/lib/cli-storybook/src/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { isAbsolute, join } from 'node:path';
import {
JsPackageManagerFactory,
type PackageManagerName,
prompt,
serverRequire,
syncStorybookAddons,
versions,
} from 'storybook/internal/common';
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';

Expand Down Expand Up @@ -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?`,
});

Expand Down
14 changes: 9 additions & 5 deletions code/lib/cli-storybook/src/autoblock/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -12,8 +12,11 @@ vi.mock('node:fs/promises', async (importOriginal) => ({
...(await importOriginal<any>()),
writeFile: vi.fn(),
}));
vi.mock('boxen', () => ({
default: vi.fn((x) => x),
vi.mock('storybook/internal/common', async (importOriginal) => ({
...(await importOriginal<any>()),
prompt: {
logBox: vi.fn((x) => x),
},
}));
vi.mock('storybook/internal/node-logger', () => ({
logger: {
Expand All @@ -24,6 +27,7 @@ vi.mock('storybook/internal/node-logger', () => ({
}));

const logger = vi.mocked(loggerRaw);
const prompt = vi.mocked(promptRaw);

const blockers = {
alwaysPass: createBlocker({
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
18 changes: 8 additions & 10 deletions code/lib/cli-storybook/src/autoblock/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -183,12 +183,10 @@ export const rendererToFramework: Fix<MigrationResult> = {
async run(options: RunOptions<MigrationResult>) {
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,
});
Comment on lines +186 to 190
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Consider adding error handling for the case where prompt.text() fails or returns undefined

Suggested change
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 glob = await prompt.text({
message:
'Enter a custom glob pattern to scan for story files (or press enter to use default):',
initialValue: defaultGlob,
}) || defaultGlob;


const projectRoot = getProjectRoot();
Expand Down
Loading