diff --git a/code/addons/themes/src/postinstall.ts b/code/addons/themes/src/postinstall.ts index 991529112e7c..3d99b67f3d37 100644 --- a/code/addons/themes/src/postinstall.ts +++ b/code/addons/themes/src/postinstall.ts @@ -1,4 +1,4 @@ -import { spawn } from 'child_process'; +import { spawnSync } from 'child_process'; const PACKAGE_MANAGER_TO_COMMAND = { npm: 'npx', @@ -12,11 +12,11 @@ const selectPackageManagerCommand = (packageManager: string) => PACKAGE_MANAGER_TO_COMMAND[packageManager as keyof typeof PACKAGE_MANAGER_TO_COMMAND]; export default async function postinstall({ packageManager = 'npm' }) { - const command = selectPackageManagerCommand(packageManager); + const commandString = selectPackageManagerCommand(packageManager); + const [command, ...commandArgs] = commandString.split(' '); - await spawn(`${command} @storybook/auto-config themes`, { + spawnSync(command, [...commandArgs, '@storybook/auto-config', 'themes'], { stdio: 'inherit', cwd: process.cwd(), - shell: true, }); } diff --git a/code/addons/vitest/src/postinstall.ts b/code/addons/vitest/src/postinstall.ts index bb1d6df2b7d3..1fd721d44d1c 100644 --- a/code/addons/vitest/src/postinstall.ts +++ b/code/addons/vitest/src/postinstall.ts @@ -46,6 +46,7 @@ export default async function postInstall(options: PostinstallOptions) { ); const vitestVersionSpecifier = await packageManager.getInstalledVersion('vitest'); + logger.debug(`Vitest version specifier: ${vitestVersionSpecifier}`); const isVitest3_2To4 = vitestVersionSpecifier ? satisfies(vitestVersionSpecifier, '>=3.2.0 <4.0.0') : false; @@ -328,13 +329,14 @@ export default async function postInstall(options: PostinstallOptions) { 'storybook', 'automigrate', 'addon-a11y-addon-test', - '--loglevel=silent', + '--loglevel', + 'silent', '--yes', '--skip-doctor', ]; if (options.packageManager) { - command.push(`--package-manager=${options.packageManager}`); + command.push('--package-manager', options.packageManager); } if (options.skipInstall) { @@ -342,7 +344,7 @@ export default async function postInstall(options: PostinstallOptions) { } if (options.configDir !== '.storybook') { - command.push(`--config-dir="${options.configDir}"`); + command.push('--config-dir', options.configDir); } await prompt.executeTask( diff --git a/code/addons/vitest/src/vitest-plugin/global-setup.ts b/code/addons/vitest/src/vitest-plugin/global-setup.ts index 098810f87b91..2d11a8afbff5 100644 --- a/code/addons/vitest/src/vitest-plugin/global-setup.ts +++ b/code/addons/vitest/src/vitest-plugin/global-setup.ts @@ -47,7 +47,6 @@ const startStorybookIfNotRunning = async () => { storybookProcess = spawn(storybookScript, [], { stdio: process.env.DEBUG === 'storybook' ? 'pipe' : 'ignore', cwd: process.cwd(), - shell: true, }); storybookProcess.on('error', (error) => { diff --git a/code/core/package.json b/code/core/package.json index f165c07cc143..8749f22e7696 100644 --- a/code/core/package.json +++ b/code/core/package.json @@ -265,7 +265,6 @@ "bundle-require": "^5.1.0", "camelcase": "^8.0.0", "chai": "^5.1.1", - "cli-table3": "^0.6.1", "commander": "^14.0.1", "comment-parser": "^1.4.1", "copy-to-clipboard": "^3.3.1", diff --git a/code/core/src/bin/dispatcher.ts b/code/core/src/bin/dispatcher.ts index 71cc19f5e076..f5a9e15a32cb 100644 --- a/code/core/src/bin/dispatcher.ts +++ b/code/core/src/bin/dispatcher.ts @@ -61,7 +61,7 @@ async function run() { if (targetCliPackageJson.version === versions[targetCli.pkg]) { command = [ 'node', - `"${join(resolvePackageDir(targetCli.pkg), 'dist/bin/index.js')}"`, + join(resolvePackageDir(targetCli.pkg), 'dist/bin/index.js'), ...targetCli.args, ]; } @@ -70,7 +70,7 @@ async function run() { } command ??= ['npx', '--yes', `${targetCli.pkg}@${versions[targetCli.pkg]}`, ...targetCli.args]; - const child = spawn(command[0], command.slice(1), { stdio: 'inherit', shell: true }); + const child = spawn(command[0], command.slice(1), { stdio: 'inherit' }); child.on('exit', (code) => { process.exit(code); }); diff --git a/code/core/src/builder-manager/index.ts b/code/core/src/builder-manager/index.ts index ae06b0d8dcc2..7daba1650222 100644 --- a/code/core/src/builder-manager/index.ts +++ b/code/core/src/builder-manager/index.ts @@ -141,7 +141,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({ router, }) { if (!options.quiet) { - logger.info('Starting manager..'); + logger.info('Starting...'); } const { diff --git a/code/core/src/cli/AddonVitestService.test.ts b/code/core/src/cli/AddonVitestService.test.ts index 62cddbfa7847..d045b0d37223 100644 --- a/code/core/src/cli/AddonVitestService.test.ts +++ b/code/core/src/cli/AddonVitestService.test.ts @@ -390,7 +390,7 @@ describe('AddonVitestService', () => { }); expect(prompt.executeTaskWithSpinner).toHaveBeenCalledWith(expect.any(Function), { id: 'playwright-installation', - intro: 'Installing Playwright browser binaries', + intro: 'Installing Playwright browser binaries (Press "c" to abort)', error: expect.stringContaining('An error occurred'), success: 'Playwright browser binaries installed successfully', abortable: true, diff --git a/code/core/src/cli/AddonVitestService.ts b/code/core/src/cli/AddonVitestService.ts index b97aad7a37d6..94a06ec66ba1 100644 --- a/code/core/src/cli/AddonVitestService.ts +++ b/code/core/src/cli/AddonVitestService.ts @@ -126,7 +126,7 @@ export class AddonVitestService { : await (async () => { logger.log(dedent` Playwright browser binaries are necessary for @storybook/addon-vitest. The download can take some time. If you don't want to wait, you can skip the installation and run the following command manually later: - ${CLI_COLORS.cta(playwrightCommand.join(' '))} + ${CLI_COLORS.cta(`npx ${playwrightCommand.join(' ')}`)} `); return prompt.confirm({ message: 'Do you want to install Playwright with Chromium now?', @@ -139,12 +139,12 @@ export class AddonVitestService { (signal) => packageManager.runPackageCommand({ args: playwrightCommand, - stdio: 'ignore', + stdio: ['inherit', 'pipe', 'pipe'], signal, }), { id: 'playwright-installation', - intro: 'Installing Playwright browser binaries', + intro: 'Installing Playwright browser binaries (Press "c" to abort)', error: `An error occurred while installing Playwright browser binaries. Please run the following command later: npx ${playwrightCommand.join(' ')}`, success: 'Playwright browser binaries installed successfully', abortable: true, diff --git a/code/core/src/common/js-package-manager/JsPackageManager.test.ts b/code/core/src/common/js-package-manager/JsPackageManager.test.ts index 7c0b0c5e12aa..3156f14cf1ec 100644 --- a/code/core/src/common/js-package-manager/JsPackageManager.test.ts +++ b/code/core/src/common/js-package-manager/JsPackageManager.test.ts @@ -2,22 +2,23 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { JsPackageManager } from './JsPackageManager'; +const mockVersions = vi.hoisted(() => ({ + '@storybook/react': '8.3.0', +})); + vi.mock('../versions', () => ({ - default: { - '@storybook/react': '8.3.0', - }, + default: mockVersions, })); describe('JsPackageManager', () => { let jsPackageManager: JsPackageManager; - let mockLatestVersion: ReturnType; + let mockLatestVersion: ReturnType; beforeEach(() => { - mockLatestVersion = vi.fn(); - // @ts-expect-error Ignore abstract class error jsPackageManager = new JsPackageManager(); - jsPackageManager.latestVersion = mockLatestVersion; + // @ts-expect-error latestVersion is a method that exists on the instance + mockLatestVersion = vi.spyOn(jsPackageManager, 'latestVersion'); vi.clearAllMocks(); }); diff --git a/code/core/src/common/js-package-manager/JsPackageManager.ts b/code/core/src/common/js-package-manager/JsPackageManager.ts index 38c4dbee3d86..76b4f1c2d9e9 100644 --- a/code/core/src/common/js-package-manager/JsPackageManager.ts +++ b/code/core/src/common/js-package-manager/JsPackageManager.ts @@ -9,7 +9,7 @@ import { type ExecaChildProcess } from 'execa'; // eslint-disable-next-line depend/ban-dependencies import { globSync } from 'glob'; import picocolors from 'picocolors'; -import { gt, satisfies } from 'semver'; +import { coerce, gt, satisfies } from 'semver'; import invariant from 'tiny-invariant'; import { HandledError } from '../utils/HandledError'; @@ -635,10 +635,13 @@ export abstract class JsPackageManager { const version = Object.entries(installations.dependencies)[0]?.[1]?.[0].version || null; + const coercedVersion = coerce(version, { includePrerelease: true })?.toString() ?? version; + + logger.debug(`Installed version for ${packageName}: ${coercedVersion}`); // Cache the result - JsPackageManager.installedVersionCache.set(cacheKey, version); + JsPackageManager.installedVersionCache.set(cacheKey, coercedVersion); - return version; + return coercedVersion; } catch (e) { JsPackageManager.installedVersionCache.set(cacheKey, null); return null; diff --git a/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts b/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts index d7917cb9909d..d8f8376f1cf7 100644 --- a/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts +++ b/code/core/src/common/js-package-manager/JsPackageManagerFactory.ts @@ -1,8 +1,8 @@ import { basename, parse, relative } from 'node:path'; -import { sync as spawnSync } from 'cross-spawn'; import * as find from 'empathic/find'; +import { executeCommandSync } from '../utils/command'; import { getProjectRoot } from '../utils/paths'; import { BUNProxy } from './BUNProxy'; import type { JsPackageManager, PackageManagerName } from './JsPackageManager'; @@ -195,56 +195,70 @@ export class JsPackageManagerFactory { } function hasNPM(cwd?: string) { - const npmVersionCommand = spawnSync('npm --version', { - cwd, - shell: true, - env: { - ...process.env, - ...COMMON_ENV_VARS, - }, - }); - return npmVersionCommand.status === 0; + try { + executeCommandSync({ + command: 'npm', + args: ['--version'], + cwd, + env: { + ...process.env, + ...COMMON_ENV_VARS, + }, + }); + return true; + } catch (err) { + return false; + } } function hasBun(cwd?: string) { - const pnpmVersionCommand = spawnSync('bun --version', { - cwd, - shell: true, - env: { - ...process.env, - ...COMMON_ENV_VARS, - }, - }); - return pnpmVersionCommand.status === 0; + try { + executeCommandSync({ + command: 'bun', + args: ['--version'], + cwd, + env: { + ...process.env, + ...COMMON_ENV_VARS, + }, + }); + return true; + } catch (err) { + return false; + } } function hasPNPM(cwd?: string) { - const pnpmVersionCommand = spawnSync('pnpm --version', { - cwd, - shell: true, - env: { - ...process.env, - ...COMMON_ENV_VARS, - }, - }); - return pnpmVersionCommand.status === 0; + try { + executeCommandSync({ + command: 'pnpm', + args: ['--version'], + cwd, + env: { + ...process.env, + ...COMMON_ENV_VARS, + }, + }); + + return true; + } catch (err) { + return false; + } } function getYarnVersion(cwd?: string): 1 | 2 | undefined { - const yarnVersionCommand = spawnSync('yarn --version', { - cwd, - shell: true, - env: { - ...process.env, - ...COMMON_ENV_VARS, - }, - }); - - if (yarnVersionCommand.status !== 0) { + try { + const yarnVersion = executeCommandSync({ + command: 'yarn', + args: ['--version'], + cwd, + env: { + ...process.env, + ...COMMON_ENV_VARS, + }, + }); + return /^1\.+/.test(yarnVersion.trim()) ? 1 : 2; + } catch (err) { return undefined; } - - const yarnVersion = yarnVersionCommand.output.toString().replace(/,/g, '').replace(/"/g, ''); - - return /^1\.+/.test(yarnVersion) ? 1 : 2; } diff --git a/code/core/src/common/js-package-manager/Yarn2Proxy.ts b/code/core/src/common/js-package-manager/Yarn2Proxy.ts index b1723c99c162..3d0331a73e33 100644 --- a/code/core/src/common/js-package-manager/Yarn2Proxy.ts +++ b/code/core/src/common/js-package-manager/Yarn2Proxy.ts @@ -11,6 +11,7 @@ import * as find from 'empathic/find'; // eslint-disable-next-line depend/ban-dependencies import type { ExecaChildProcess } from 'execa'; +import { logger } from '../../node-logger'; import type { ExecuteCommandOptions } from '../utils/command'; import { executeCommand } from '../utils/command'; import { getProjectRoot } from '../utils/paths'; @@ -136,6 +137,8 @@ export class Yarn2Proxy extends JsPackageManager { }); const commandResult = childProcess.stdout ?? ''; + logger.debug(`Installation found for ${pattern.join(', ')}: ${commandResult}`); + return this.mapDependencies(commandResult, pattern); } catch (e) { return undefined; @@ -280,6 +283,7 @@ export class Yarn2Proxy extends JsPackageManager { const duplicatedDependencies: Record = {}; lines.forEach((packageName) => { + logger.debug(`Processing package ${packageName}`); if ( !packageName || !pattern.some((p) => new RegExp(`${p.replace(/\*/g, '.*')}`).test(packageName)) @@ -288,6 +292,7 @@ export class Yarn2Proxy extends JsPackageManager { } const { name, value } = parsePackageData(packageName.replaceAll(`"`, '')); + logger.debug(`Package ${name} found with version ${value.version}`); if (!existingVersions[name]?.includes(value.version)) { if (acc[name]) { acc[name].push(value); diff --git a/code/core/src/common/utils/command.ts b/code/core/src/common/utils/command.ts index 8cb638575cec..b525637ec32d 100644 --- a/code/core/src/common/utils/command.ts +++ b/code/core/src/common/utils/command.ts @@ -1,7 +1,7 @@ import { logger, prompt } from 'storybook/internal/node-logger'; // eslint-disable-next-line depend/ban-dependencies -import { type CommonOptions, type ExecaChildProcess, execa } from 'execa'; +import { type CommonOptions, type ExecaChildProcess, execa, execaCommandSync } from 'execa'; const COMMON_ENV_VARS = { COREPACK_ENABLE_STRICT: '0', @@ -22,7 +22,6 @@ function getExecaOptions({ stdio, cwd, env, ...execaOptions }: ExecuteCommandOpt cwd, stdio: stdio ?? prompt.getPreferredStdio(), encoding: 'utf8' as const, - shell: true, cleanup: true, env: { ...COMMON_ENV_VARS, @@ -45,3 +44,16 @@ export function executeCommand(options: ExecuteCommandOptions): ExecaChildProces return execaProcess; } + +export function executeCommandSync(options: ExecuteCommandOptions): string { + const { command, args = [], ignoreError = false } = options; + try { + const commandResult = execaCommandSync([command, ...args].join(' '), getExecaOptions(options)); + return commandResult.stdout ?? ''; + } catch (err) { + if (!ignoreError) { + throw err; + } + return ''; + } +} diff --git a/code/core/src/core-server/dev-server.ts b/code/core/src/core-server/dev-server.ts index 44c90385b0e8..1a67dedd3f92 100644 --- a/code/core/src/core-server/dev-server.ts +++ b/code/core/src/core-server/dev-server.ts @@ -94,9 +94,7 @@ export async function storybookDevServer(options: Options) { await Promise.resolve(); if (!options.ignorePreview) { - if (!options.quiet) { - logger.info('Starting preview..'); - } + logger.debug('Starting preview..'); previewResult = await previewBuilder .start({ startTime: process.hrtime(), diff --git a/code/core/src/core-server/utils/output-startup-information.ts b/code/core/src/core-server/utils/output-startup-information.ts index 8460cb192e11..f86cf5d050cd 100644 --- a/code/core/src/core-server/utils/output-startup-information.ts +++ b/code/core/src/core-server/utils/output-startup-information.ts @@ -1,7 +1,6 @@ import { CLI_COLORS, logger } from 'storybook/internal/node-logger'; import type { VersionCheck } from 'storybook/internal/types'; -import Table from 'cli-table3'; import picocolors from 'picocolors'; import prettyTime from 'pretty-hrtime'; import { dedent } from 'ts-dedent'; @@ -22,34 +21,22 @@ export function outputStartupInformation(options: { const updateMessage = createUpdateMessage(updateInfo, version); - const serveMessage = new Table({ - chars: { - top: '', - 'top-mid': '', - 'top-left': '', - 'top-right': '', - bottom: '', - 'bottom-mid': '', - 'bottom-left': '', - 'bottom-right': '', - left: '', - 'left-mid': '', - mid: '', - 'mid-mid': '', - right: '', - 'right-mid': '', - middle: '', - }, - // @ts-expect-error (Converted from ts-ignore) - paddingLeft: 0, - paddingRight: 0, - paddingTop: 0, - paddingBottom: 0, - }); + const serverMessages = [ + `- Local: ${address}`, + `- On your network: ${networkAddress}`, + ]; - serveMessage.push( - ['Local:', picocolors.cyan(address)], - ['On your network:', picocolors.cyan(networkAddress)] + logger.logBox( + dedent` + Storybook ready! + + ${serverMessages.join('\n')}${updateMessage ? `\n\n${updateMessage}` : ''} + `, + { + formatBorder: CLI_COLORS.storybook, + contentPadding: 3, + rounded: true, + } ); const timeStatement = [ @@ -59,14 +46,5 @@ export function outputStartupInformation(options: { .filter(Boolean) .join(' and '); - logger.logBox( - dedent` - ${CLI_COLORS.success( - `Storybook ${picocolors.bold(version)} for ${picocolors.bold(name)} started` - )} - ${timeStatement} - - ${serveMessage.toString()}${updateMessage ? `\n\n${updateMessage}` : ''} - ` - ); + logger.info(timeStatement); } diff --git a/code/core/src/core-server/utils/server-statics.ts b/code/core/src/core-server/utils/server-statics.ts index a152f35feccd..966fdd2f5789 100644 --- a/code/core/src/core-server/utils/server-statics.ts +++ b/code/core/src/core-server/utils/server-statics.ts @@ -119,7 +119,7 @@ export async function useStatics(app: Polka, options: Options): Promise { // Don't log for internal static dirs if (!targetEndpoint.startsWith('/sb-') && !staticDir.startsWith(cacheDir)) { const relativeStaticDir = relative(getProjectRoot(), staticDir); - logger.info( + logger.debug( `Serving static files from ${CLI_COLORS.info(relativeStaticDir)} at ${CLI_COLORS.info(targetEndpoint)}` ); } diff --git a/code/core/src/core-server/withTelemetry.ts b/code/core/src/core-server/withTelemetry.ts index c3dbc8f7aee6..8538060ba38b 100644 --- a/code/core/src/core-server/withTelemetry.ts +++ b/code/core/src/core-server/withTelemetry.ts @@ -194,10 +194,12 @@ export async function withTelemetry( throw error; } finally { - const errors = ErrorCollector.getErrors(); - for (const error of errors) { - await sendTelemetryError(error, eventType, options, false); + if (enableTelemetry) { + const errors = ErrorCollector.getErrors(); + for (const error of errors) { + await sendTelemetryError(error, eventType, options, false); + } + process.off('SIGINT', cancelTelemetry); } - process.off('SIGINT', cancelTelemetry); } } diff --git a/code/core/src/node-logger/logger/colors.ts b/code/core/src/node-logger/logger/colors.ts index ee5e5d92c1ec..68c010b8e59e 100644 --- a/code/core/src/node-logger/logger/colors.ts +++ b/code/core/src/node-logger/logger/colors.ts @@ -9,4 +9,5 @@ export const CLI_COLORS = { // Only color a link if it is the primary call to action, otherwise links shouldn't be colored cta: picocolors.cyan, muted: picocolors.dim, + storybook: (text: string) => `\x1b[38;2;255;71;133m${text}\x1b[39m`, }; diff --git a/code/core/src/node-logger/logger/logger.ts b/code/core/src/node-logger/logger/logger.ts index 7cda501b492a..a6faf8bb95a8 100644 --- a/code/core/src/node-logger/logger/logger.ts +++ b/code/core/src/node-logger/logger/logger.ts @@ -162,14 +162,8 @@ export const error = createLogger('error', (...args: LogFunctionArgs { if (shouldLog('info')) { diff --git a/code/core/src/node-logger/prompts/prompt-functions.ts b/code/core/src/node-logger/prompts/prompt-functions.ts index bcec0cfddfa6..f78c5696c9ad 100644 --- a/code/core/src/node-logger/prompts/prompt-functions.ts +++ b/code/core/src/node-logger/prompts/prompt-functions.ts @@ -157,47 +157,35 @@ export const spinner = (options: SpinnerOptions): SpinnerInstance => { }; export const taskLog = (options: TaskLogOptions): TaskLogInstance => { - if (isInteractiveTerminal()) { + if (isInteractiveTerminal() || shouldLog('info')) { const task = getPromptProvider().taskLog(options); // Wrap the task log methods to handle console.log patching const wrappedTaskLog: TaskLogInstance = { message: (message: string) => { - if (shouldLog('info')) { - task.message(wrapTextForClack(message)); - } + task.message(wrapTextForClack(message)); }, success: (message: string, options?: { showLog?: boolean }) => { activeTaskLog = null; restoreConsoleLog(); - if (shouldLog('info')) { - task.success(message, options); - } + task.success(message, options); }, error: (message: string) => { activeTaskLog = null; restoreConsoleLog(); - if (shouldLog('error')) { - task.error(message); - } + task.error(message); }, group: function (title: string) { this.message(`\n${title}\n`); return { message: (message: string) => { - if (shouldLog('info')) { - task.message(wrapTextForClack(message)); - } + task.message(wrapTextForClack(message)); }, success: (message: string) => { - if (shouldLog('info')) { - task.success(message); - } + task.success(message); }, error: (message: string) => { - if (shouldLog('error')) { - task.error(message); - } + task.error(message); }, }; }, @@ -209,27 +197,29 @@ export const taskLog = (options: TaskLogOptions): TaskLogInstance => { return wrappedTaskLog; } else { + const maybeLog = shouldLog('info') ? logger.log : (_: string) => {}; + return { message: (message: string) => { - logger.log(message); + maybeLog(message); }, success: (message: string) => { - logger.log(message); + maybeLog(message); }, error: (message: string) => { - logger.log(message); + maybeLog(message); }, group: (title: string) => { - logger.log(`\n${title}\n`); + maybeLog(`\n${title}\n`); return { message: (message: string) => { - logger.log(message); + maybeLog(message); }, success: (message: string) => { - logger.log(message); + maybeLog(message); }, error: (message: string) => { - logger.log(message); + maybeLog(message); }, }; }, diff --git a/code/core/src/node-logger/tasks.ts b/code/core/src/node-logger/tasks.ts index 4b586f07dbcd..cbd5b67569e5 100644 --- a/code/core/src/node-logger/tasks.ts +++ b/code/core/src/node-logger/tasks.ts @@ -69,7 +69,6 @@ export const executeTask = async ( let cleanup: (() => void) | undefined; if (abortable) { - log(CLI_COLORS.info('Press "c" to abort')); const result = setupAbortController(); abortController = result.abortController; cleanup = result.cleanup; @@ -127,7 +126,6 @@ export const executeTaskWithSpinner = async ( let cleanup: (() => void) | undefined; if (abortable) { - log(CLI_COLORS.info('Press "c" to abort')); const result = setupAbortController(); abortController = result.abortController; cleanup = result.cleanup; diff --git a/code/core/src/node-logger/wrap-utils.ts b/code/core/src/node-logger/wrap-utils.ts index f15348f27e27..8382c58bb2d4 100644 --- a/code/core/src/node-logger/wrap-utils.ts +++ b/code/core/src/node-logger/wrap-utils.ts @@ -1,6 +1,4 @@ import { S_BAR } from '@clack/prompts'; -// eslint-disable-next-line depend/ban-dependencies -import { execaSync } from 'execa'; import { cyan, dim, reset } from 'picocolors'; import wrapAnsi from 'wrap-ansi'; @@ -32,7 +30,7 @@ function getVisibleLength(str: string): number { } function getEnvFromTerminal(key: string): string { - return execaSync('echo', [`$${key}`], { shell: true }).stdout.trim(); + return (process.env[key] || '').trim(); } /** @@ -62,8 +60,8 @@ function supportsHyperlinks(): boolean { // Most other modern terminals support hyperlinks return true; } - } catch (error) { - // If we can't execute shell commands, fall back to conservative default + } catch { + // If we can't access environment variables, fall back to conservative default return false; } } diff --git a/code/core/src/telemetry/exec-command-count-lines.ts b/code/core/src/telemetry/exec-command-count-lines.ts index fdc4547ce464..2399f94d43d9 100644 --- a/code/core/src/telemetry/exec-command-count-lines.ts +++ b/code/core/src/telemetry/exec-command-count-lines.ts @@ -14,7 +14,7 @@ export async function execCommandCountLines( command: string, options?: Parameters[1] ) { - const process = execaCommand(command, { shell: true, buffer: false, ...options }); + const process = execaCommand(command, { buffer: false, ...options }); if (!process.stdout) { // eslint-disable-next-line local-rules/no-uncategorized-errors throw new Error('Unexpected missing stdout'); diff --git a/code/core/src/telemetry/notify.ts b/code/core/src/telemetry/notify.ts index 63bea1b4b666..db2260def91a 100644 --- a/code/core/src/telemetry/notify.ts +++ b/code/core/src/telemetry/notify.ts @@ -19,7 +19,7 @@ export const notify = async () => { logger.log( dedent` - ${CLI_COLORS.info('Attention:')} Storybook collects completely anonymous telemetry regarding usage. This information is used to shape Storybook's roadmap and prioritize features. You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL: + Attention: Storybook now collects completely anonymous telemetry regarding usage. This information is used to shape Storybook's roadmap and prioritize features. You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL: https://storybook.js.org/telemetry ` ); diff --git a/code/lib/cli-storybook/src/bin/run.ts b/code/lib/cli-storybook/src/bin/run.ts index a4779699f028..6a916e49e190 100644 --- a/code/lib/cli-storybook/src/bin/run.ts +++ b/code/lib/cli-storybook/src/bin/run.ts @@ -264,7 +264,7 @@ command('automigrate [fixId]') .option('--skip-doctor', 'Skip doctor check') .action(async (fixId, options) => { withTelemetry('automigrate', { cliOptions: options }, async () => { - logger.intro(`Running ${fixId} automigration`); + logger.intro(fixId ? `Running ${fixId} automigration` : 'Running automigrations'); await doAutomigrate({ fixId, ...options }); logger.outro('Done'); }).catch(handleCommandFailure(options.logfile)); diff --git a/code/lib/cli-storybook/src/link.ts b/code/lib/cli-storybook/src/link.ts index 99fbbaea0df4..65f06c0a4418 100644 --- a/code/lib/cli-storybook/src/link.ts +++ b/code/lib/cli-storybook/src/link.ts @@ -1,12 +1,10 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { basename, extname, join } from 'node:path'; +import { executeCommand } from 'storybook/internal/common'; import { logger } from 'storybook/internal/node-logger'; -import { spawn as spawnAsync, sync as spawnSync } from 'cross-spawn'; -import picocolors from 'picocolors'; - -type ExecOptions = Parameters[2]; +import { sync as spawnSync } from 'cross-spawn'; interface LinkOptions { target: string; @@ -14,50 +12,6 @@ interface LinkOptions { start: boolean; } -// TODO: Extract this to somewhere else, or use `exec` from a different file that might already have it -export const exec = async ( - command: string, - options: ExecOptions = {}, - { - startMessage, - errorMessage, - dryRun, - }: { startMessage?: string; errorMessage?: string; dryRun?: boolean } = {} -) => { - if (startMessage) { - logger.info(startMessage); - } - - if (dryRun) { - logger.info(`\n> ${command}\n`); - return undefined; - } - - logger.info(command); - return new Promise((resolve, reject) => { - const child = spawnAsync(command, { - ...options, - shell: true, - stdio: 'pipe', - }); - - child.stderr.pipe(process.stdout); - child.stdout.pipe(process.stdout); - - child.on('exit', (code) => { - if (code === 0) { - resolve(undefined); - } else { - logger.error(picocolors.red(`An error occurred while executing: \`${command}\``)); - if (errorMessage) { - logger.info(errorMessage); - } - reject(new Error(`command exited with code: ${code}: `)); - } - }); - }); -}; - export const link = async ({ target, local, start }: LinkOptions) => { const storybookDir = process.cwd(); try { @@ -80,7 +34,11 @@ export const link = async ({ target, local, start }: LinkOptions) => { await mkdir(reprosDir, { recursive: true }); logger.info(`Cloning ${target}`); - await exec(`git clone ${target}`, { cwd: reprosDir }); + await executeCommand({ + command: 'git', + args: ['clone', target], + cwd: reprosDir, + }); // Extract a repro name from url given as input (take the last part of the path and remove the extension) reproName = basename(target, extname(target)); reproDir = join(reprosDir, reproName); @@ -101,7 +59,11 @@ export const link = async ({ target, local, start }: LinkOptions) => { } logger.info(`Linking ${reproDir}`); - await exec(`yarn link --all --relative "${storybookDir}"`, { cwd: reproDir }); + await executeCommand({ + command: 'yarn', + args: ['link', '--all', '--relative', storybookDir], + cwd: reproDir, + }); logger.info(`Installing ${reproName}`); @@ -124,10 +86,18 @@ export const link = async ({ target, local, start }: LinkOptions) => { await writeFile(join(reproDir, 'package.json'), JSON.stringify(reproPackageJson, null, 2)); - await exec(`yarn install`, { cwd: reproDir }); + await executeCommand({ + command: 'yarn', + args: ['install'], + cwd: reproDir, + }); if (start) { logger.info(`Running ${reproName} storybook`); - await exec(`yarn run storybook`, { cwd: reproDir }); + await executeCommand({ + command: 'yarn', + args: ['run', 'storybook'], + cwd: reproDir, + }); } }; diff --git a/code/lib/cli-storybook/src/sandbox.ts b/code/lib/cli-storybook/src/sandbox.ts index 826123f7be66..9e4467f6260c 100644 --- a/code/lib/cli-storybook/src/sandbox.ts +++ b/code/lib/cli-storybook/src/sandbox.ts @@ -1,5 +1,5 @@ import { existsSync } from 'node:fs'; -import { mkdir, readdir, rm } from 'node:fs/promises'; +import { readdir, rm } from 'node:fs/promises'; import { isAbsolute } from 'node:path'; import type { PackageManagerName } from 'storybook/internal/common'; @@ -52,7 +52,6 @@ export const sandbox = async ({ const currentVersion = versions.storybook; const isPrerelease = prerelease(currentVersion); const isOutdated = lt(currentVersion, isPrerelease ? nextVersion : latestVersion); - const borderColor = isOutdated ? '#FC521F' : '#F1618C'; const downloadType = !isOutdated && init ? 'after-storybook' : 'before-storybook'; const branch = isPrerelease ? 'next' : 'main'; @@ -78,7 +77,9 @@ export const sandbox = async ({ .concat(init && (isOutdated || isPrerelease) ? [messages.longInitTime] : []) .concat(isPrerelease ? [messages.prerelease] : []) .join('\n'), - { borderStyle: 'round', borderColor } + { + rounded: true, + } ); if (!selectedConfig) { @@ -255,7 +256,7 @@ export const sandbox = async ({ Having a clean repro helps us solve your issue faster! 🙏 `.trim(), - { borderStyle: 'round', borderColor: '#F1618C' } + { rounded: true } ); } 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 1106e679c2c0..9d587e48c4b2 100644 --- a/code/lib/cli-storybook/src/upgrade.ts +++ b/code/lib/cli-storybook/src/upgrade.ts @@ -74,7 +74,7 @@ const formatPackage = (pkg: Package) => `${pkg.package}@${pkg.version}`; const warnPackages = (pkgs: Package[]) => pkgs.map((pkg) => `- ${formatPackage(pkg)}`).join('\n'); export const checkVersionConsistency = () => { - const lines = spawnSync('npm ls', { stdio: 'pipe', shell: true }).output.toString().split('\n'); + const lines = spawnSync('npm', ['ls'], { stdio: 'pipe' }).output.toString().split('\n'); const storybookPackages = lines .map(getStorybookVersion) .filter((item): item is NonNullable => !!item) diff --git a/code/lib/codemod/src/index.ts b/code/lib/codemod/src/index.ts index 6bb0b93be03a..9ed9c6686700 100644 --- a/code/lib/codemod/src/index.ts +++ b/code/lib/codemod/src/index.ts @@ -90,7 +90,6 @@ export async function runCodemod( ], { stdio: 'inherit', - shell: true, } ); diff --git a/code/lib/create-storybook/src/commands/FinalizationCommand.test.ts b/code/lib/create-storybook/src/commands/FinalizationCommand.test.ts index f20b28151c5e..c36207798038 100644 --- a/code/lib/create-storybook/src/commands/FinalizationCommand.test.ts +++ b/code/lib/create-storybook/src/commands/FinalizationCommand.test.ts @@ -4,7 +4,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { getProjectRoot } from 'storybook/internal/common'; import { logger } from 'storybook/internal/node-logger'; -import { Feature } from 'storybook/internal/types'; import * as find from 'empathic/find'; @@ -35,10 +34,7 @@ describe('FinalizationCommand', () => { vi.mocked(fs.readFile).mockResolvedValue('node_modules/\n' as any); vi.mocked(fs.appendFile).mockResolvedValue(undefined); - const selectedFeatures = new Set([Feature.DOCS, Feature.TEST]); - await command.execute({ - selectedFeatures, storybookCommand: 'npm run storybook', }); @@ -53,10 +49,7 @@ describe('FinalizationCommand', () => { it('should not update gitignore if file not found', async () => { vi.mocked(find.up).mockReturnValue(undefined); - const selectedFeatures = new Set([]); - await command.execute({ - selectedFeatures, storybookCommand: 'yarn storybook', }); @@ -69,10 +62,7 @@ describe('FinalizationCommand', () => { vi.mocked(find.up).mockReturnValue('/other/path/.gitignore'); vi.mocked(getProjectRoot).mockReturnValue('/test/project'); - const selectedFeatures = new Set([]); - await command.execute({ - selectedFeatures, storybookCommand: 'npm run storybook', }); @@ -86,10 +76,7 @@ describe('FinalizationCommand', () => { 'node_modules/\n*storybook.log\nstorybook-static\n' as any ); - const selectedFeatures = new Set([]); - await command.execute({ - selectedFeatures, storybookCommand: 'npm run storybook', }); @@ -101,10 +88,7 @@ describe('FinalizationCommand', () => { vi.mocked(fs.readFile).mockResolvedValue('node_modules/\n*storybook.log\n' as any); vi.mocked(fs.appendFile).mockResolvedValue(undefined); - const selectedFeatures = new Set([]); - await command.execute({ - selectedFeatures, storybookCommand: 'npm run storybook', }); @@ -114,41 +98,10 @@ describe('FinalizationCommand', () => { ); }); - it('should print features as "none" when no features selected', async () => { - vi.mocked(find.up).mockReturnValue(undefined); - - const selectedFeatures = new Set([]); - - await command.execute({ - selectedFeatures, - storybookCommand: 'npm run storybook', - }); - - expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('Additional features: none')); - }); - - it('should print all selected features', async () => { - vi.mocked(find.up).mockReturnValue(undefined); - - const selectedFeatures = new Set([Feature.DOCS, Feature.TEST, Feature.ONBOARDING]); - - await command.execute({ - selectedFeatures, - storybookCommand: 'npm run storybook', - }); - - expect(logger.log).toHaveBeenCalledWith( - expect.stringContaining('Additional features: docs, test, onboarding') - ); - }); - it('should include storybook command in output', async () => { vi.mocked(find.up).mockReturnValue(undefined); - const selectedFeatures = new Set([]); - await command.execute({ - selectedFeatures, storybookCommand: 'ng run my-app:storybook', }); diff --git a/code/lib/create-storybook/src/commands/FinalizationCommand.ts b/code/lib/create-storybook/src/commands/FinalizationCommand.ts index 80fa771056b0..b7fe8efe9b1c 100644 --- a/code/lib/create-storybook/src/commands/FinalizationCommand.ts +++ b/code/lib/create-storybook/src/commands/FinalizationCommand.ts @@ -3,13 +3,11 @@ import fs from 'node:fs/promises'; import { getProjectRoot } from 'storybook/internal/common'; import { CLI_COLORS, logTracker, logger } from 'storybook/internal/node-logger'; import { ErrorCollector } from 'storybook/internal/telemetry'; -import type { Feature } from 'storybook/internal/types'; import * as find from 'empathic/find'; import { dedent } from 'ts-dedent'; type ExecuteFinalizationParams = { - selectedFeatures: Set; storybookCommand?: string | null; }; @@ -26,16 +24,16 @@ type ExecuteFinalizationParams = { export class FinalizationCommand { constructor(private logfile: string | boolean | undefined) {} /** Execute finalization steps */ - async execute({ selectedFeatures, storybookCommand }: ExecuteFinalizationParams): Promise { + async execute({ storybookCommand }: ExecuteFinalizationParams): Promise { // Update .gitignore await this.updateGitignore(); const errors = ErrorCollector.getErrors(); if (errors.length > 0) { - await this.printFailureMessage(selectedFeatures, storybookCommand); + await this.printFailureMessage(storybookCommand); } else { - this.printSuccessMessage(selectedFeatures, storybookCommand); + this.printSuccessMessage(storybookCommand); } } @@ -64,31 +62,21 @@ export class FinalizationCommand { } } - private async printFailureMessage( - selectedFeatures: Set, - storybookCommand?: string | null - ): Promise { + private async printFailureMessage(storybookCommand?: string | null): Promise { logger.warn('Storybook setup completed, but some non-blocking errors occurred.'); - this.printNextSteps(selectedFeatures, storybookCommand); + this.printNextSteps(storybookCommand); const logFile = await logTracker.writeToFile(this.logfile); logger.warn(`Storybook debug logs can be found at: ${logFile}`); } /** Print success message with feature summary */ - private printSuccessMessage( - selectedFeatures: Set, - storybookCommand?: string | null - ): void { + private printSuccessMessage(storybookCommand?: string | null): void { logger.step(CLI_COLORS.success('Storybook was successfully installed in your project!')); - this.printNextSteps(selectedFeatures, storybookCommand); + this.printNextSteps(storybookCommand); } - private printNextSteps(selectedFeatures: Set, storybookCommand?: string | null): void { - const printFeatures = (features: Set) => Array.from(features).join(', ') || 'none'; - - logger.log(`Additional features: ${printFeatures(selectedFeatures)}`); - + private printNextSteps(storybookCommand?: string | null): void { if (storybookCommand) { logger.log( `To run Storybook manually, run ${CLI_COLORS.cta(storybookCommand)}. CTRL+C to stop.` diff --git a/code/lib/create-storybook/src/initiate.ts b/code/lib/create-storybook/src/initiate.ts index c873601745c2..ca78761f1ca9 100644 --- a/code/lib/create-storybook/src/initiate.ts +++ b/code/lib/create-storybook/src/initiate.ts @@ -105,7 +105,6 @@ export async function doInitiate(options: CommandOptions): Promise< // Step 8: Print final summary await executeFinalization({ logfile: options.logfile, - selectedFeatures, storybookCommand, }); @@ -148,7 +147,7 @@ export async function initiate(options: CommandOptions): Promise { async () => { const result = await doInitiate(options); - logger.outro('Initiation completed'); + logger.outro(''); return result; } @@ -179,6 +178,10 @@ async function runStorybookDev(result: { const flags = []; + if (packageManager.type === 'npm') { + flags.push('--silent'); + } + // npm needs extra -- to pass flags to the command // in the case of Angular, we are calling `ng run` which doesn't need the extra `--` if (packageManager.type === 'npm' && projectType !== ProjectType.ANGULAR) { diff --git a/code/lib/create-storybook/src/scaffold-new-project.ts b/code/lib/create-storybook/src/scaffold-new-project.ts index efdd02f16770..e0897608ca48 100644 --- a/code/lib/create-storybook/src/scaffold-new-project.ts +++ b/code/lib/create-storybook/src/scaffold-new-project.ts @@ -123,18 +123,29 @@ export const scaffoldNewProject = async ( if (!projectStrategy) { projectStrategy = await prompt.select({ - message: dedent` - Empty directory detected: - Would you like to generate a new project from the following list? - Storybook supports many more frameworks and bundlers than listed below. If you don't see your preferred setup, you can still generate a project then rerun this command to add Storybook. - `, - options: Object.entries(SUPPORTED_PROJECTS).map(([key, value]) => ({ - label: buildProjectDisplayNameForPrint(value), - value: key, - })), + message: 'Empty directory detected:', + options: [ + ...Object.entries(SUPPORTED_PROJECTS).map(([key, value]) => ({ + label: buildProjectDisplayNameForPrint(value), + value: key, + })), + { + label: 'Other', + value: 'other', + hint: 'To install Storybook on another framework, first generate a project with that framework and then rerun this command.', + }, + ], }); } + if (projectStrategy === 'other') { + logger.warn( + 'To install Storybook on another framework, first generate a project with that framework and then rerun this command.' + ); + logger.outro('Exiting...'); + process.exit(1); + } + const projectStrategyConfig = SUPPORTED_PROJECTS[projectStrategy]; const projectDisplayName = buildProjectDisplayNameForPrint(projectStrategyConfig); const createScript = projectStrategyConfig.createScript[packageManagerName]; @@ -167,7 +178,6 @@ export const scaffoldNewProject = async ( spinner.message(`Executing ${createScript}`); await execa.command(createScript, { stdio: 'pipe', - shell: true, cwd: targetDir, cleanup: true, }); diff --git a/code/yarn.lock b/code/yarn.lock index 9065aeee49d4..0717f1b67b39 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -2125,13 +2125,6 @@ __metadata: languageName: node linkType: hard -"@colors/colors@npm:1.5.0": - version: 1.5.0 - resolution: "@colors/colors@npm:1.5.0" - checksum: 10c0/eb42729851adca56d19a08e48d5a1e95efd2a32c55ae0323de8119052be0510d4b7a1611f2abcbf28c044a6c11e6b7d38f99fccdad7429300c37a8ea5fb95b44 - languageName: node - linkType: hard - "@design-systems/utils@npm:2.12.0": version: 2.12.0 resolution: "@design-systems/utils@npm:2.12.0" @@ -12877,19 +12870,6 @@ __metadata: languageName: node linkType: hard -"cli-table3@npm:^0.6.1": - version: 0.6.5 - resolution: "cli-table3@npm:0.6.5" - dependencies: - "@colors/colors": "npm:1.5.0" - string-width: "npm:^4.2.0" - dependenciesMeta: - "@colors/colors": - optional: true - checksum: 10c0/d7cc9ed12212ae68241cc7a3133c52b844113b17856e11f4f81308acc3febcea7cc9fd298e70933e294dd642866b29fd5d113c2c098948701d0c35f09455de78 - languageName: node - linkType: hard - "cli-truncate@npm:^3.1.0": version: 3.1.0 resolution: "cli-truncate@npm:3.1.0" @@ -26035,7 +26015,6 @@ __metadata: bundle-require: "npm:^5.1.0" camelcase: "npm:^8.0.0" chai: "npm:^5.1.1" - cli-table3: "npm:^0.6.1" commander: "npm:^14.0.1" comment-parser: "npm:^1.4.1" copy-to-clipboard: "npm:^3.3.1" diff --git a/scripts/tasks/sandbox-parts.ts b/scripts/tasks/sandbox-parts.ts index 359a31be9a1a..b02777c87e54 100644 --- a/scripts/tasks/sandbox-parts.ts +++ b/scripts/tasks/sandbox-parts.ts @@ -197,9 +197,8 @@ export const init: Task['run'] = async ( await executeCLIStep(steps.init, { cwd, optionValues: { - debug, + loglevel: debug ? 'debug' : 'info', yes: true, - 'skip-install': true, ...extra, ...(template.initOptions || {}), }, diff --git a/scripts/utils/cli-step.ts b/scripts/utils/cli-step.ts index f9944a85b291..05b809b11cc5 100644 --- a/scripts/utils/cli-step.ts +++ b/scripts/utils/cli-step.ts @@ -41,7 +41,7 @@ export const steps = { options: createOptions({ yes: { type: 'boolean' }, type: { type: 'string' }, - debug: { type: 'boolean' }, + loglevel: { type: 'string' }, builder: { type: 'string' }, 'skip-install': { type: 'boolean' }, }),