From 3488712ff591c6be7bd1b4aff9d56ecd424649e5 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Thu, 11 Jan 2024 12:41:03 +0100 Subject: [PATCH 1/2] ensure sandbox cli command uses latest version of template with the current version of the CLI --- code/lib/cli/src/generate.ts | 2 +- code/lib/cli/src/initiate.ts | 2 +- code/lib/cli/src/sandbox.ts | 100 ++++++++++++++++++++++++++++------- 3 files changed, 82 insertions(+), 22 deletions(-) diff --git a/code/lib/cli/src/generate.ts b/code/lib/cli/src/generate.ts index fa9acb1f7f09..677ec7100a90 100644 --- a/code/lib/cli/src/generate.ts +++ b/code/lib/cli/src/generate.ts @@ -146,7 +146,7 @@ command('sandbox [filterValue]') .option('-b --branch ', 'Define the branch to download from', 'next') .option('--no-init', 'Whether to download a template without an initialized Storybook', false) .action((filterValue, options) => - sandbox({ filterValue, ...options }).catch((e) => { + sandbox({ filterValue, ...options }, pkg).catch((e) => { logger.error(e); process.exit(1); }) diff --git a/code/lib/cli/src/initiate.ts b/code/lib/cli/src/initiate.ts index f32b824ab97b..175bbcb7e189 100644 --- a/code/lib/cli/src/initiate.ts +++ b/code/lib/cli/src/initiate.ts @@ -224,7 +224,7 @@ const projectTypeInquirer = async ( process.exit(0); }; -async function doInitiate( +export async function doInitiate( options: CommandOptions, pkg: PackageJson ): Promise< diff --git a/code/lib/cli/src/sandbox.ts b/code/lib/cli/src/sandbox.ts index 397fda11830f..aaa5f47443ca 100644 --- a/code/lib/cli/src/sandbox.ts +++ b/code/lib/cli/src/sandbox.ts @@ -7,8 +7,13 @@ import { downloadTemplate } from 'giget'; import { existsSync, readdir } from 'fs-extra'; import invariant from 'tiny-invariant'; +import { lt, prerelease } from 'semver'; import type { Template, TemplateKey } from './sandbox-templates'; import { allTemplates as TEMPLATES } from './sandbox-templates'; +import type { PackageJson, PackageManagerName } from './js-package-manager'; +import { JsPackageManagerFactory } from './js-package-manager'; +import versions from './versions'; +import { doInitiate, initiate } from './initiate'; const logger = console; @@ -17,20 +22,59 @@ interface SandboxOptions { output?: string; branch?: string; init?: boolean; + packageManager: PackageManagerName; } type Choice = keyof typeof TEMPLATES; const toChoices = (c: Choice): prompts.Choice => ({ title: TEMPLATES[c].name, value: c }); -export const sandbox = async ({ - output: outputDirectory, - filterValue, - branch, - init, -}: SandboxOptions) => { +export const sandbox = async ( + { output: outputDirectory, filterValue, branch, init, ...options }: SandboxOptions, + pkg: PackageJson +) => { // Either get a direct match when users pass a template id, or filter through all templates let selectedConfig: Template | undefined = TEMPLATES[filterValue as TemplateKey]; - let selectedTemplate: Choice | null = selectedConfig ? (filterValue as TemplateKey) : null; + let templateId: Choice | null = selectedConfig ? (filterValue as TemplateKey) : null; + + const { packageManager: pkgMgr } = options; + + const packageManager = JsPackageManagerFactory.getPackageManager({ + force: pkgMgr, + }); + const latestVersion = await packageManager.latestVersion('@storybook/cli'); + const currentVersion = versions['@storybook/cli']; + const isPrerelease = prerelease(currentVersion); + const isOutdated = lt(currentVersion, latestVersion); + const borderColor = isOutdated ? '#FC521F' : '#F1618C'; + + const downloadType = + !isOutdated && !isPrerelease && init ? 'after-storybook' : 'before-storybook'; + + const messages = { + welcome: `Creating a Storybook ${chalk.bold(currentVersion)} sandbox..`, + notLatest: chalk.red(dedent` + This version is behind the latest release, which is: ${chalk.bold(latestVersion)}! + You likely ran the init command through npx, which can use a locally cached version, to get the latest please run: + ${chalk.bold('npx storybook@latest sandbox')} + + You may want to CTRL+C to stop, and run with the latest version instead. + `), + longInitTime: chalk.yellow( + 'The creation of the sandbox will take longer, because we will need to run init.' + ), + prerelease: chalk.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 } + ) + ); if (!selectedConfig) { const filterRegex = new RegExp(`^${filterValue || ''}`, 'i'); @@ -79,7 +123,7 @@ export const sandbox = async ({ } if (choices.length === 1) { - [selectedTemplate] = choices; + [templateId] = choices; } else { logger.info( boxen( @@ -97,16 +141,16 @@ export const sandbox = async ({ ) ); - selectedTemplate = await promptSelectedTemplate(choices); + templateId = await promptSelectedTemplate(choices); } - const hasSelectedTemplate = !!(selectedTemplate ?? null); + const hasSelectedTemplate = !!(templateId ?? null); if (!hasSelectedTemplate) { logger.error('Somehow we got no templates. Please rerun this command!'); return; } - selectedConfig = selectedTemplate ? TEMPLATES[selectedTemplate] : undefined; + selectedConfig = templateId ? TEMPLATES[templateId] : undefined; if (!selectedConfig) { throw new Error('🚨 Sandbox: please specify a valid template type'); @@ -114,7 +158,7 @@ export const sandbox = async ({ } let selectedDirectory = outputDirectory; - const outputDirectoryName = outputDirectory || selectedTemplate; + const outputDirectoryName = outputDirectory || templateId; if (selectedDirectory && existsSync(`${selectedDirectory}`)) { logger.info(`⚠️ ${selectedDirectory} already exists! Overwriting...`); } @@ -151,20 +195,33 @@ export const sandbox = async ({ logger.log('📦 Downloading sandbox template...'); try { - const templateType = init ? 'after-storybook' : 'before-storybook'; // Download the sandbox based on subfolder "after-storybook" and selected branch - const gitPath = `github:storybookjs/sandboxes/${selectedTemplate}/${templateType}#${branch}`; + const gitPath = `github:storybookjs/sandboxes/${templateId}/${downloadType}#${branch}`; await downloadTemplate(gitPath, { force: true, dir: templateDestination, }); // throw an error if templateDestination is an empty directory using fs-extra if ((await readdir(templateDestination)).length === 0) { - throw new Error( - dedent`Template downloaded from ${chalk.blue(gitPath)} is empty. - Are you use it exists? Or did you want to set ${chalk.yellow( - selectedTemplate - )} to inDevelopment first?` + const selected = chalk.yellow(templateId); + throw new Error(dedent` + Template downloaded from ${chalk.blue(gitPath)} is empty. + Are you use it exists? Or did you want to set ${selected} to inDevelopment first? + `); + } + + // when user wanted an sandbox that has been initiated, but force-downloaded the before-storybook directory + // then we need to initiate the sandbox + // this is to ensure we DO get the latest version of the template (output of the generator), but we initialize using the version of storybook that the CLI is. + // we warned the user about the fact they are running an old version of storybook + // we warned the user the sandbox step would take longer + if ((isOutdated || isPrerelease) && init) { + // we run doInitiate, instead of initiate, to avoid sending this init event to telemetry, because it's not a real world project + await doInitiate( + { + ...options, + }, + pkg ); } } catch (err) { @@ -173,7 +230,10 @@ export const sandbox = async ({ } const initMessage = init - ? chalk.yellow(`yarn install\nyarn storybook`) + ? chalk.yellow(dedent` + yarn install + yarn storybook + `) : `Recreate your setup, then ${chalk.yellow(`npx storybook@latest init`)}`; logger.info( From e24f0bae5ef2008f000f3e84d874691947a63bca Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Thu, 11 Jan 2024 13:49:10 +0100 Subject: [PATCH 2/2] make it use the correct branch on the sandbox repo make it use the sandbox for pre-releases is that's "latest" --- code/lib/cli/src/generate.ts | 1 - code/lib/cli/src/sandbox.ts | 13 +++++++------ scripts/tasks/sandbox-parts.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/code/lib/cli/src/generate.ts b/code/lib/cli/src/generate.ts index 677ec7100a90..a2ad12bd3439 100644 --- a/code/lib/cli/src/generate.ts +++ b/code/lib/cli/src/generate.ts @@ -143,7 +143,6 @@ command('sandbox [filterValue]') .alias('repro') // for backwards compatibility .description('Create a sandbox from a set of possible templates') .option('-o --output ', 'Define an output directory') - .option('-b --branch ', 'Define the branch to download from', 'next') .option('--no-init', 'Whether to download a template without an initialized Storybook', false) .action((filterValue, options) => sandbox({ filterValue, ...options }, pkg).catch((e) => { diff --git a/code/lib/cli/src/sandbox.ts b/code/lib/cli/src/sandbox.ts index aaa5f47443ca..8bc306d39049 100644 --- a/code/lib/cli/src/sandbox.ts +++ b/code/lib/cli/src/sandbox.ts @@ -13,7 +13,7 @@ import { allTemplates as TEMPLATES } from './sandbox-templates'; import type { PackageJson, PackageManagerName } from './js-package-manager'; import { JsPackageManagerFactory } from './js-package-manager'; import versions from './versions'; -import { doInitiate, initiate } from './initiate'; +import { doInitiate } from './initiate'; const logger = console; @@ -29,7 +29,7 @@ type Choice = keyof typeof TEMPLATES; const toChoices = (c: Choice): prompts.Choice => ({ title: TEMPLATES[c].name, value: c }); export const sandbox = async ( - { output: outputDirectory, filterValue, branch, init, ...options }: SandboxOptions, + { output: outputDirectory, filterValue, init, ...options }: SandboxOptions, pkg: PackageJson ) => { // Either get a direct match when users pass a template id, or filter through all templates @@ -42,13 +42,14 @@ export const sandbox = async ( force: pkgMgr, }); const latestVersion = await packageManager.latestVersion('@storybook/cli'); + const nextVersion = await packageManager.latestVersion('@storybook/cli@next'); const currentVersion = versions['@storybook/cli']; const isPrerelease = prerelease(currentVersion); - const isOutdated = lt(currentVersion, latestVersion); + const isOutdated = lt(currentVersion, isPrerelease ? nextVersion : latestVersion); const borderColor = isOutdated ? '#FC521F' : '#F1618C'; - const downloadType = - !isOutdated && !isPrerelease && init ? 'after-storybook' : 'before-storybook'; + const downloadType = !isOutdated && init ? 'after-storybook' : 'before-storybook'; + const branch = isPrerelease ? 'next' : 'main'; const messages = { welcome: `Creating a Storybook ${chalk.bold(currentVersion)} sandbox..`, @@ -193,7 +194,7 @@ export const sandbox = async ( logger.info(`🏃 Adding ${selectedConfig.name} into ${templateDestination}`); - logger.log('📦 Downloading sandbox template...'); + logger.log(`📦 Downloading sandbox template (${chalk.bold(downloadType)})...`); try { // Download the sandbox based on subfolder "after-storybook" and selected branch const gitPath = `github:storybookjs/sandboxes/${templateId}/${downloadType}#${branch}`; diff --git a/scripts/tasks/sandbox-parts.ts b/scripts/tasks/sandbox-parts.ts index 575d58bfb08b..42b40379186d 100644 --- a/scripts/tasks/sandbox-parts.ts +++ b/scripts/tasks/sandbox-parts.ts @@ -69,7 +69,7 @@ export const create: Task['run'] = async ({ key, template, sandboxDir }, { dryRu } else { await executeCLIStep(steps.repro, { argument: key, - optionValues: { output: sandboxDir, branch: 'next', init: false, debug }, + optionValues: { output: sandboxDir, init: false, debug }, cwd: parentDir, dryRun, debug,