From 741858169fc5dd550966788df9d81e31277bba17 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 9 Jan 2024 15:53:04 +0100 Subject: [PATCH 01/13] apply https://github.com/storybookjs/storybook/pull/25517 on main --- code/lib/cli/package.json | 1 - code/lib/cli/src/generators/NEXTJS/index.ts | 2 +- code/lib/cli/src/generators/REACT/index.ts | 2 +- .../cli/src/generators/REACT_NATIVE/index.ts | 4 +- .../cli/src/generators/WEBPACK_REACT/index.ts | 2 +- code/lib/cli/src/generators/baseGenerator.ts | 11 +++--- code/lib/cli/src/initiate.ts | 39 ++++++++++++++----- .../js-package-manager/JsPackageManager.ts | 18 ++++++++- code/yarn.lock | 10 ----- 9 files changed, 57 insertions(+), 32 deletions(-) diff --git a/code/lib/cli/package.json b/code/lib/cli/package.json index d9149f4900ef..0e97f59161f6 100644 --- a/code/lib/cli/package.json +++ b/code/lib/cli/package.json @@ -91,7 +91,6 @@ "puppeteer-core": "^2.1.1", "read-pkg-up": "^7.0.1", "semver": "^7.3.7", - "simple-update-notifier": "^2.0.0", "strip-json-comments": "^3.0.1", "tempy": "^1.0.1", "ts-dedent": "^2.0.0", diff --git a/code/lib/cli/src/generators/NEXTJS/index.ts b/code/lib/cli/src/generators/NEXTJS/index.ts index 2588b387312a..15852e30b171 100644 --- a/code/lib/cli/src/generators/NEXTJS/index.ts +++ b/code/lib/cli/src/generators/NEXTJS/index.ts @@ -9,7 +9,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => { { ...options, builder: CoreBuilder.Webpack5 }, 'react', { - extraAddons: ['@storybook/addon-onboarding'], + extraAddons: ['@storybook/addon-onboarding@1.0.0'], }, 'nextjs' ); diff --git a/code/lib/cli/src/generators/REACT/index.ts b/code/lib/cli/src/generators/REACT/index.ts index 046860356c56..d5e415b3863b 100644 --- a/code/lib/cli/src/generators/REACT/index.ts +++ b/code/lib/cli/src/generators/REACT/index.ts @@ -11,7 +11,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => { await baseGenerator(packageManager, npmOptions, options, 'react', { extraPackages, useSWC: ({ builder }) => builder === CoreBuilder.Webpack5, - extraAddons: ['@storybook/addon-onboarding'], + extraAddons: ['@storybook/addon-onboarding@1.0.0'], }); }; diff --git a/code/lib/cli/src/generators/REACT_NATIVE/index.ts b/code/lib/cli/src/generators/REACT_NATIVE/index.ts index dc3e14ed0f7e..dcdfb037c242 100644 --- a/code/lib/cli/src/generators/REACT_NATIVE/index.ts +++ b/code/lib/cli/src/generators/REACT_NATIVE/index.ts @@ -30,14 +30,14 @@ const generator = async ( '@storybook/addon-controls@^6.5.16', ]; - const resolvedPackages = await packageManager.getVersionedPackages(packagesToResolve); + const versionedPackages = await packageManager.getVersionedPackages(packagesToResolve); const babelDependencies = await getBabelDependencies(packageManager, packageJson); const packages = [ ...babelDependencies, ...packagesWithFixedVersion, - ...resolvedPackages, + ...versionedPackages, missingReactDom && reactVersion && `react-dom@${reactVersion}`, ].filter(Boolean); diff --git a/code/lib/cli/src/generators/WEBPACK_REACT/index.ts b/code/lib/cli/src/generators/WEBPACK_REACT/index.ts index 94e8babee466..3393392ce3e5 100644 --- a/code/lib/cli/src/generators/WEBPACK_REACT/index.ts +++ b/code/lib/cli/src/generators/WEBPACK_REACT/index.ts @@ -4,7 +4,7 @@ import type { Generator } from '../types'; const generator: Generator = async (packageManager, npmOptions, options) => { await baseGenerator(packageManager, npmOptions, options, 'react', { - extraAddons: ['@storybook/addon-onboarding'], + extraAddons: ['@storybook/addon-onboarding@1.0.0'], useSWC: ({ builder }) => builder === CoreBuilder.Webpack5, }); }; diff --git a/code/lib/cli/src/generators/baseGenerator.ts b/code/lib/cli/src/generators/baseGenerator.ts index 6083fae9e873..59988e9f9819 100644 --- a/code/lib/cli/src/generators/baseGenerator.ts +++ b/code/lib/cli/src/generators/baseGenerator.ts @@ -328,8 +328,6 @@ export async function baseGenerator( const versionedPackages = await packageManager.getVersionedPackages(packages); versionedPackagesSpinner.succeed(); - const depsToInstall = [...versionedPackages]; - // Add basic babel config for a select few frameworks that need it, if they do not have a babel config file already if (builder !== CoreBuilder.Vite && !skipBabel) { const frameworksThatNeedBabelConfig = [ @@ -349,7 +347,7 @@ export async function baseGenerator( if (hasNoBabelFile && needsBabelConfig) { const isTypescript = language !== SupportedLanguage.JAVASCRIPT; const isReact = rendererId === 'react'; - depsToInstall.push( + versionedPackages.push( ...getBabelPresets({ typescript: isTypescript, jsx: isReact, @@ -370,7 +368,7 @@ export async function baseGenerator( if (hasEslint && !isStorybookPluginInstalled) { if (skipPrompts || (await suggestESLintPlugin())) { - depsToInstall.push('eslint-plugin-storybook'); + versionedPackages.push('eslint-plugin-storybook'); await configureEslintPlugin(eslintConfigFile, packageManager); } } @@ -379,12 +377,13 @@ export async function baseGenerator( // any failure regarding configuring the eslint plugin should not fail the whole generator } - if (depsToInstall.length > 0) { + if (versionedPackages.length > 0) { const addDependenciesSpinner = ora({ indent: 2, text: 'Installing Storybook dependencies', }).start(); - await packageManager.addDependencies({ ...npmOptions, packageJson }, depsToInstall); + + await packageManager.addDependencies({ ...npmOptions, packageJson }, versionedPackages); addDependenciesSpinner.succeed(); } diff --git a/code/lib/cli/src/initiate.ts b/code/lib/cli/src/initiate.ts index 3afd703a1e08..2adbe8c5bc03 100644 --- a/code/lib/cli/src/initiate.ts +++ b/code/lib/cli/src/initiate.ts @@ -9,6 +9,7 @@ import { NxProjectDetectedError } from '@storybook/core-events/server-errors'; import dedent from 'ts-dedent'; import boxen from 'boxen'; import { readdirSync } from 'fs-extra'; +import { lt, prerelease } from 'semver'; import { installableProjectTypes, ProjectType } from './project_types'; import { detect, isStorybookInstantiated, detectLanguage, detectPnp } from './detect'; import { commandLog, codeLog, paddedLog } from './helpers'; @@ -35,6 +36,7 @@ import { JsPackageManagerFactory, useNpmWarning } from './js-package-manager'; import type { NpmOptions } from './NpmOptions'; import type { CommandOptions, GeneratorOptions } from './generators/types'; import { HandledError } from './HandledError'; +import versions from './versions'; const logger = console; @@ -296,15 +298,6 @@ async function doInitiate( cwdFolderEntries.length === 0 || cwdFolderEntries.every((entry) => entry.startsWith('.')); const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); - const welcomeMessage = 'storybook init - the simplest way to add a Storybook to your project.'; - logger.log(chalk.inverse(`\n ${welcomeMessage} \n`)); - - // Update notify code. - const { default: updateNotifier } = await import('simple-update-notifier'); - await updateNotifier({ - pkg: pkg as any, - updateCheckInterval: 1000 * 60 * 60, // every hour (we could increase this later on.) - }); if (options.force !== true && isEmptyDir) { logger.log( @@ -317,6 +310,34 @@ async function doInitiate( throw new HandledError('Project was initialized in an empty directory.'); } + 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 messages = { + welcome: `Adding Storybook version ${chalk.bold(currentVersion)} to your project..`, + 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 init')} + + You may want to CTRL+C to stop, and run with the latest version instead. + `), + prelease: chalk.yellow('This is a pre-release version.'), + }; + + logger.log( + boxen( + [messages.welcome] + .concat(isOutdated && !isPrerelease ? [messages.notLatest] : []) + .concat(isPrerelease ? [messages.prelease] : []) + .join('\n'), + { borderStyle: 'round', padding: 1, borderColor } + ) + ); + let projectType: ProjectType; const projectTypeProvided = options.type; const infoText = projectTypeProvided diff --git a/code/lib/cli/src/js-package-manager/JsPackageManager.ts b/code/lib/cli/src/js-package-manager/JsPackageManager.ts index 1cb3d93ba8c7..52c0e53d1455 100644 --- a/code/lib/cli/src/js-package-manager/JsPackageManager.ts +++ b/code/lib/cli/src/js-package-manager/JsPackageManager.ts @@ -325,13 +325,29 @@ export abstract class JsPackageManager { /** * Return an array of strings matching following format: `@` * + * For packages in the storybook monorepo, when the latest version is equal to the version of the current CLI + * the version is not added to the string. + * + * When a package is in the monorepo, and the version is not equal to the CLI version, the version is taken from the versions.ts file and added to the string. + * * @param packages */ public getVersionedPackages(packages: string[]): Promise { return Promise.all( packages.map(async (pkg) => { const [packageName, packageVersion] = getPackageDetails(pkg); - return `${packageName}@${await this.getVersion(packageName, packageVersion)}`; + const latestInRange = await this.latestVersion(packageName, packageVersion); + + const k = packageName as keyof typeof storybookPackagesVersions; + const currentVersion = storybookPackagesVersions[k]; + + if (currentVersion === latestInRange) { + return `${packageName}`; + } + if (currentVersion) { + return `${packageName}@${currentVersion}`; + } + return `${packageName}@^${latestInRange}`; }) ); } diff --git a/code/yarn.lock b/code/yarn.lock index c616fb2a47fd..68700a300f6a 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6678,7 +6678,6 @@ __metadata: puppeteer-core: "npm:^2.1.1" read-pkg-up: "npm:^7.0.1" semver: "npm:^7.3.7" - simple-update-notifier: "npm:^2.0.0" slash: "npm:^5.0.0" strip-json-comments: "npm:^3.1.1" tempy: "npm:^1.0.1" @@ -28086,15 +28085,6 @@ __metadata: languageName: node linkType: hard -"simple-update-notifier@npm:^2.0.0": - version: 2.0.0 - resolution: "simple-update-notifier@npm:2.0.0" - dependencies: - semver: "npm:^7.5.3" - checksum: 2a00bd03bfbcbf8a737c47ab230d7920f8bfb92d1159d421bdd194479f6d01ebc995d13fbe13d45dace23066a78a3dc6642999b4e3b38b847e6664191575b20c - languageName: node - linkType: hard - "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5" From dbf6a9ddc74ccae84405e854636007526d17d2a3 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 10 Jan 2024 14:39:25 +0100 Subject: [PATCH 02/13] add version modifier --- code/lib/cli/src/generators/NEXTJS/index.ts | 2 +- code/lib/cli/src/generators/REACT/index.ts | 2 +- code/lib/cli/src/generators/WEBPACK_REACT/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/code/lib/cli/src/generators/NEXTJS/index.ts b/code/lib/cli/src/generators/NEXTJS/index.ts index 15852e30b171..33ac4faff8aa 100644 --- a/code/lib/cli/src/generators/NEXTJS/index.ts +++ b/code/lib/cli/src/generators/NEXTJS/index.ts @@ -9,7 +9,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => { { ...options, builder: CoreBuilder.Webpack5 }, 'react', { - extraAddons: ['@storybook/addon-onboarding@1.0.0'], + extraAddons: ['@storybook/addon-onboarding@^1.0.0'], }, 'nextjs' ); diff --git a/code/lib/cli/src/generators/REACT/index.ts b/code/lib/cli/src/generators/REACT/index.ts index d5e415b3863b..436c0dabd730 100644 --- a/code/lib/cli/src/generators/REACT/index.ts +++ b/code/lib/cli/src/generators/REACT/index.ts @@ -11,7 +11,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => { await baseGenerator(packageManager, npmOptions, options, 'react', { extraPackages, useSWC: ({ builder }) => builder === CoreBuilder.Webpack5, - extraAddons: ['@storybook/addon-onboarding@1.0.0'], + extraAddons: ['@storybook/addon-onboarding@^1.0.0'], }); }; diff --git a/code/lib/cli/src/generators/WEBPACK_REACT/index.ts b/code/lib/cli/src/generators/WEBPACK_REACT/index.ts index 3393392ce3e5..b3d317376d0e 100644 --- a/code/lib/cli/src/generators/WEBPACK_REACT/index.ts +++ b/code/lib/cli/src/generators/WEBPACK_REACT/index.ts @@ -4,7 +4,7 @@ import type { Generator } from '../types'; const generator: Generator = async (packageManager, npmOptions, options) => { await baseGenerator(packageManager, npmOptions, options, 'react', { - extraAddons: ['@storybook/addon-onboarding@1.0.0'], + extraAddons: ['@storybook/addon-onboarding@^1.0.0'], useSWC: ({ builder }) => builder === CoreBuilder.Webpack5, }); }; From 6f6ed73158f011dab8b342b636a6f41c3270ec9f Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Thu, 11 Jan 2024 23:13:48 +0100 Subject: [PATCH 03/13] new upgrade workflow based on https://github.com/storybookjs/storybook/pull/25553 --- code/lib/cli/src/generate.ts | 4 +- .../js-package-manager/JsPackageManager.ts | 2 + code/lib/cli/src/upgrade.test.ts | 87 +++------ code/lib/cli/src/upgrade.ts | 178 +++++++++--------- .../core-events/src/errors/server-errors.ts | 47 +++++ 5 files changed, 161 insertions(+), 157 deletions(-) diff --git a/code/lib/cli/src/generate.ts b/code/lib/cli/src/generate.ts index 8f37de772933..be3c4da1175e 100644 --- a/code/lib/cli/src/generate.ts +++ b/code/lib/cli/src/generate.ts @@ -73,7 +73,7 @@ command('babelrc') .action(() => generateStorybookBabelConfigInCWD()); command('upgrade') - .description('Upgrade your Storybook packages to the latest') + .description(`Upgrade your Storybook packages to v${versions.storybook}`) .option( '--package-manager ', 'Force package manager for installing dependencies' @@ -81,8 +81,6 @@ command('upgrade') .option('-N --use-npm', 'Use NPM to install dependencies (deprecated)') .option('-y --yes', 'Skip prompting the user') .option('-n --dry-run', 'Only check for upgrades, do not install') - .option('-t --tag ', 'Upgrade to a certain npm dist-tag (e.g. next, prerelease)') - .option('-p --prerelease', 'Upgrade to the pre-release packages') .option('-s --skip-check', 'Skip postinstall version and automigration checks') .option('-c, --config-dir ', 'Directory where to load Storybook configurations from') .action(async (options: UpgradeOptions) => upgrade(options).catch(() => process.exit(1))); diff --git a/code/lib/cli/src/js-package-manager/JsPackageManager.ts b/code/lib/cli/src/js-package-manager/JsPackageManager.ts index 52c0e53d1455..fca583935c84 100644 --- a/code/lib/cli/src/js-package-manager/JsPackageManager.ts +++ b/code/lib/cli/src/js-package-manager/JsPackageManager.ts @@ -133,6 +133,8 @@ export abstract class JsPackageManager { done = commandLog('Installing dependencies'); + logger.log(); + try { await this.runInstall(); done(); diff --git a/code/lib/cli/src/upgrade.test.ts b/code/lib/cli/src/upgrade.test.ts index 1110946e0b2b..d9445b66fcfb 100644 --- a/code/lib/cli/src/upgrade.test.ts +++ b/code/lib/cli/src/upgrade.test.ts @@ -1,4 +1,20 @@ -import { addExtraFlags, addNxPackagesToReject, getStorybookVersion } from './upgrade'; +import { getStorybookCoreVersion } from '@storybook/telemetry'; +import { + UpgradeStorybookToLowerVersionError, + UpgradeStorybookToSameVersionError, +} from '@storybook/core-events/server-errors'; +import { doUpgrade, getStorybookVersion } from './upgrade'; + +jest.mock('@storybook/telemetry'); +jest.mock('./versions', () => { + const originalVersions = jest.requireActual('./versions').default; + return { + default: Object.keys(originalVersions).reduce((acc, key) => { + acc[key] = '8.0.0'; + return acc; + }, {} as Record), + }; +}); describe.each([ ['│ │ │ ├── @babel/code-frame@7.10.3 deduped', null], @@ -21,68 +37,15 @@ describe.each([ }); }); -describe('extra flags', () => { - const extraFlags = { - 'react-scripts@<5': ['--foo'], - }; - const devDependencies = {}; - it('package matches constraints', () => { - expect( - addExtraFlags(extraFlags, [], { dependencies: { 'react-scripts': '4' }, devDependencies }) - ).toEqual(['--foo']); - }); - it('package prerelease matches constraints', () => { - expect( - addExtraFlags(extraFlags, [], { - dependencies: { 'react-scripts': '4.0.0-alpha.0' }, - devDependencies, - }) - ).toEqual(['--foo']); - }); - it('package not matches constraints', () => { - expect( - addExtraFlags(extraFlags, [], { - dependencies: { 'react-scripts': '5.0.0-alpha.0' }, - devDependencies, - }) - ).toEqual([]); - }); - it('no package not matches constraints', () => { - expect( - addExtraFlags(extraFlags, [], { - dependencies: {}, - devDependencies, - }) - ).toEqual([]); - }); -}); +describe('Upgrade errors', () => { + it('should throw an error when upgrading to a lower version number', async () => { + jest.mocked(getStorybookCoreVersion).mockResolvedValue('8.1.0'); -describe('addNxPackagesToReject', () => { - it('reject exists and is in regex pattern', () => { - const flags = ['--reject', '/preset-create-react-app/', '--some-flag', 'hello']; - expect(addNxPackagesToReject(flags)).toMatchObject([ - '--reject', - '/(preset-create-react-app|@nrwl/storybook|@nx/storybook)/', - '--some-flag', - 'hello', - ]); + await expect(doUpgrade({} as any)).rejects.toThrowError(UpgradeStorybookToLowerVersionError); }); - it('reject exists and is in unknown pattern', () => { - const flags = ['--some-flag', 'hello', '--reject', '@storybook/preset-create-react-app']; - expect(addNxPackagesToReject(flags)).toMatchObject([ - '--some-flag', - 'hello', - '--reject', - '@storybook/preset-create-react-app,@nrwl/storybook,@nx/storybook', - ]); - }); - it('reject does not exist', () => { - const flags = ['--some-flag', 'hello']; - expect(addNxPackagesToReject(flags)).toMatchObject([ - '--some-flag', - 'hello', - '--reject', - '@nrwl/storybook,@nx/storybook', - ]); + it('should throw an error when upgrading to the same version number', async () => { + jest.mocked(getStorybookCoreVersion).mockResolvedValue('8.0.0'); + + await expect(doUpgrade({} as any)).rejects.toThrowError(UpgradeStorybookToSameVersionError); }); }); diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts index dd31f177df53..787998fce0a8 100644 --- a/code/lib/cli/src/upgrade.ts +++ b/code/lib/cli/src/upgrade.ts @@ -1,14 +1,22 @@ import { sync as spawnSync } from 'cross-spawn'; import { telemetry, getStorybookCoreVersion } from '@storybook/telemetry'; -import semver from 'semver'; +import semver, { coerce, eq, lt, prerelease } from 'semver'; import { logger } from '@storybook/node-logger'; import { withTelemetry } from '@storybook/core-server'; - -import type { PackageJsonWithMaybeDeps, PackageManagerName } from './js-package-manager'; -import { getPackageDetails, JsPackageManagerFactory, useNpmWarning } from './js-package-manager'; +import { + UpgradeStorybookToLowerVersionError, + UpgradeStorybookToSameVersionError, +} from '@storybook/core-events/server-errors'; + +import chalk from 'chalk'; +import dedent from 'ts-dedent'; +import boxen from 'boxen'; +import type { PackageManagerName } from './js-package-manager'; +import { JsPackageManagerFactory, useNpmWarning } from './js-package-manager'; import { commandLog } from './helpers'; import { automigrate } from './automigrate'; import { isCorePackage } from './utils'; +import versions from './versions'; type Package = { package: string; @@ -83,57 +91,7 @@ export const checkVersionConsistency = () => { }); }; -type ExtraFlags = Record; -const EXTRA_FLAGS: ExtraFlags = { - 'react-scripts@<5': ['--reject', '/preset-create-react-app/'], -}; - -export const addExtraFlags = ( - extraFlags: ExtraFlags, - flags: string[], - { dependencies, devDependencies }: PackageJsonWithMaybeDeps -) => { - return Object.entries(extraFlags).reduce( - (acc, entry) => { - const [pattern, extra] = entry; - const [pkg, specifier] = getPackageDetails(pattern); - const pkgVersion = dependencies[pkg] || devDependencies[pkg]; - - if (pkgVersion && semver.satisfies(semver.coerce(pkgVersion), specifier)) { - return [...acc, ...extra]; - } - - return acc; - }, - [...flags] - ); -}; - -export const addNxPackagesToReject = (flags: string[]) => { - const newFlags = [...flags]; - const index = flags.indexOf('--reject'); - if (index > -1) { - // Try to understand if it's in the format of a regex pattern - if (newFlags[index + 1].endsWith('/') && newFlags[index + 1].startsWith('/')) { - // Remove last and first slash so that I can add the parentheses - newFlags[index + 1] = newFlags[index + 1].substring(1, newFlags[index + 1].length - 1); - newFlags[index + 1] = `/(${newFlags[index + 1]}|@nrwl/storybook|@nx/storybook)/`; - } else { - // Adding the two packages as comma-separated values - // If the existing rejects are in regex format, they will be ignored. - // Maybe we need to find a more robust way to treat rejects? - newFlags[index + 1] = `${newFlags[index + 1]},@nrwl/storybook,@nx/storybook`; - } - } else { - newFlags.push('--reject'); - newFlags.push('@nrwl/storybook,@nx/storybook'); - } - return newFlags; -}; - export interface UpgradeOptions { - tag: string; - prerelease: boolean; skipCheck: boolean; useNpm: boolean; packageManager: PackageManagerName; @@ -144,8 +102,6 @@ export interface UpgradeOptions { } export const doUpgrade = async ({ - tag, - prerelease, skipCheck, useNpm, packageManager: pkgMgr, @@ -161,48 +117,88 @@ export const doUpgrade = async ({ } const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); + const currentVersion = versions['@storybook/cli']; const beforeVersion = await getStorybookCoreVersion(); - commandLog(`Checking for latest versions of '@storybook/*' packages`); - - if (tag && prerelease) { - throw new Error( - `Cannot set both --tag and --prerelease. Use --tag next to get the latest prereleae` - ); + if (lt(currentVersion, beforeVersion)) { + throw new UpgradeStorybookToLowerVersionError({ beforeVersion, currentVersion }); } - - let target = 'latest'; - if (prerelease) { - // '@next' is storybook's convention for the latest prerelease tag. - // This used to be 'greatest', but that was not reliable and could pick canaries, etc. - // and random releases of other packages with storybook in their name. - target = '@next'; - } else if (tag) { - target = `@${tag}`; + if (eq(currentVersion, beforeVersion)) { + throw new UpgradeStorybookToSameVersionError({ beforeVersion }); } - let flags = []; - if (!dryRun) flags.push('--upgrade'); - flags.push('--target'); - flags.push(target); - flags = addExtraFlags(EXTRA_FLAGS, flags, await packageManager.retrievePackageJson()); - flags = addNxPackagesToReject(flags); - const check = spawnSync('npx', ['npm-check-updates@latest', '/storybook/', ...flags], { - stdio: 'pipe', - shell: true, - }); - logger.info(check.stdout.toString()); - logger.info(check.stderr.toString()); + const latestVersion = await packageManager.latestVersion('@storybook/cli'); + const isOutdated = lt(currentVersion, latestVersion); + const isPrerelease = prerelease(currentVersion) !== null; + + const borderColor = isOutdated ? '#FC521F' : '#F1618C'; + + const messages = { + welcome: `Upgrading Storybook from version ${chalk.bold(beforeVersion)} to version ${chalk.bold( + currentVersion + )}..`, + notLatest: chalk.red(dedent` + This version is behind the latest release, which is: ${chalk.bold(latestVersion)}! + You likely ran the upgrade command through npx, which can use a locally cached version, to upgrade to the latest version please run: + ${chalk.bold('npx storybook@latest upgrade')} + + You may want to CTRL+C to stop, and run with the latest version instead. + `), + prelease: chalk.yellow('This is a pre-release version.'), + }; - const checkSb = spawnSync('npx', ['npm-check-updates@latest', 'sb', ...flags], { - stdio: 'pipe', - shell: true, - }); - logger.info(checkSb.stdout.toString()); - logger.info(checkSb.stderr.toString()); + logger.plain( + boxen( + [messages.welcome] + .concat(isOutdated && !isPrerelease ? [messages.notLatest] : []) + .concat(isPrerelease ? [messages.prelease] : []) + .join('\n'), + { borderStyle: 'round', padding: 1, borderColor } + ) + ); + + const packageJson = await packageManager.retrievePackageJson(); + + const toUpgradedDependencies = (deps: Record) => { + const monorepoDependencies = Object.keys(deps || {}).filter((dependency) => { + // don't upgrade @storybook/preset-create-react-app if react-scripts is < v5 + if (dependency === '@storybook/preset-create-react-app') { + const reactScriptsVersion = + packageJson.dependencies['react-scripts'] ?? packageJson.devDependencies['react-scripts']; + if (reactScriptsVersion && lt(coerce(reactScriptsVersion), '5.0.0')) { + return false; + } + } + + // only upgrade packages that are in the monorepo + return dependency in versions; + }) as Array; + return monorepoDependencies.map( + (dependency) => + // add ^ modifier to the version if this is the latest and stable version + // example output: @storybook/react@^8.0.0 + `${dependency}@${!isOutdated || isPrerelease ? '^' : ''}${versions[dependency]}` + ); + }; + + const upgradedDependencies = toUpgradedDependencies(packageJson.dependencies); + const upgradedDevDependencies = toUpgradedDependencies(packageJson.devDependencies); if (!dryRun) { - commandLog(`Installing upgrades`); + commandLog(`Updating dependencies in ${chalk.cyan('package.json')}..`); + logger.plain(''); + if (upgradedDependencies.length > 0) { + await packageManager.addDependencies( + { installAsDevDependencies: false, skipInstall: true, packageJson }, + upgradedDependencies + ); + } + if (upgradedDevDependencies.length > 0) { + await packageManager.addDependencies( + { installAsDevDependencies: true, skipInstall: true, packageJson }, + upgradedDevDependencies + ); + } await packageManager.installDependencies(); } @@ -219,8 +215,6 @@ export const doUpgrade = async ({ automigrationPreCheckFailure: preCheckFailure || null, }; telemetry('upgrade', { - prerelease, - tag, beforeVersion, afterVersion, ...automigrationTelemetry, diff --git a/code/lib/core-events/src/errors/server-errors.ts b/code/lib/core-events/src/errors/server-errors.ts index f4ecab544773..49a774006540 100644 --- a/code/lib/core-events/src/errors/server-errors.ts +++ b/code/lib/core-events/src/errors/server-errors.ts @@ -410,3 +410,50 @@ export class NoMatchingExportError extends StorybookError { `; } } + +export class UpgradeStorybookToLowerVersionError extends StorybookError { + readonly category = Category.CLI_UPGRADE; + + readonly code = 3; + + constructor(public data: { beforeVersion: string; currentVersion: string }) { + super(); + } + + template() { + return dedent` + You are trying to upgrade Storybook to a lower version than the version currently installed. This is not supported. + Storybook version ${this.data.beforeVersion} was detected in your project, but you are trying to "upgrade" to version ${this.data.currentVersion}. + + This usually happens when running the upgrade command without a version specifier, e.g. "npx storybook upgrade". + This will cause npm to run the globally cached storybook binary, which might be an older version. + Instead you should always run the Storybook CLI with a version specifier to force npm to download the latest version: + + "npx storybook@latest upgrade" + `; + } +} + +export class UpgradeStorybookToSameVersionError extends StorybookError { + readonly category = Category.CLI_UPGRADE; + + readonly code = 4; + + constructor(public data: { beforeVersion: string }) { + super(); + } + + template() { + return dedent` + You are trying to upgrade Storybook to the same version that is currently installed in the project, version ${this.data.beforeVersion}. This is not supported. + + This usually happens when running the upgrade command without a version specifier, e.g. "npx storybook upgrade". + This will cause npm to run the globally cached storybook binary, which might be the same version that you already have. + This also happens if you're running the Storybook CLI that is locally installed in your project. + If you intended to upgrade to the latest version, you should always run the Storybook CLI with a version specifier to force npm to download the latest version: + "npx storybook@latest upgrade" + If you intended to re-run automigrations, you should run the "automigrate" command directly instead: + "npx storybook@${this.data.beforeVersion} automigrate" + `; + } +} From 3f48968ab8728f229b99d338b891fc281d4a310b Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Fri, 12 Jan 2024 09:15:58 +0100 Subject: [PATCH 04/13] use current version for sandbox cli command --- code/lib/cli/src/generate.ts | 2 +- code/lib/cli/src/initiate.ts | 2 +- code/lib/cli/src/sandbox.ts | 106 ++++++++++++++++++++++++++------- scripts/tasks/sandbox-parts.ts | 2 +- 4 files changed, 86 insertions(+), 26 deletions(-) diff --git a/code/lib/cli/src/generate.ts b/code/lib/cli/src/generate.ts index be3c4da1175e..aff6dde48acb 100644 --- a/code/lib/cli/src/generate.ts +++ b/code/lib/cli/src/generate.ts @@ -151,7 +151,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 2adbe8c5bc03..2ab4fb561890 100644 --- a/code/lib/cli/src/initiate.ts +++ b/code/lib/cli/src/initiate.ts @@ -274,7 +274,7 @@ const getEmptyDirMessage = (packageManagerType: PackageManagerName) => { `; }; -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 2938414adbfa..0d8003e07bac 100644 --- a/code/lib/cli/src/sandbox.ts +++ b/code/lib/cli/src/sandbox.ts @@ -6,31 +6,75 @@ import { dedent } from 'ts-dedent'; import { downloadTemplate } from 'giget'; import { existsSync, readdir } from 'fs-extra'; +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 } from './initiate'; + const logger = console; interface SandboxOptions { filterValue?: string; 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, 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 nextVersion = await packageManager.latestVersion('@storybook/cli@next'); + const currentVersion = versions['@storybook/cli']; + 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'; + + 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'); @@ -78,7 +122,7 @@ export const sandbox = async ({ } if (choices.length === 1) { - [selectedTemplate] = choices; + [templateId] = choices; } else { logger.info( boxen( @@ -96,16 +140,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 = TEMPLATES[selectedTemplate]; + selectedConfig = TEMPLATES[templateId]; if (!selectedConfig) { throw new Error('🚨 Sandbox: please specify a valid template type'); @@ -113,7 +157,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...`); } @@ -146,23 +190,36 @@ export const sandbox = async ({ : path.join(process.cwd(), selectedDirectory); logger.info(`🏃 Adding ${selectedConfig.name} into ${templateDestination}`); + logger.log(`📦 Downloading sandbox template (${chalk.bold(downloadType)})...`); - 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) { @@ -171,7 +228,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( diff --git a/scripts/tasks/sandbox-parts.ts b/scripts/tasks/sandbox-parts.ts index 7c890c5ac8b4..564647ab319b 100644 --- a/scripts/tasks/sandbox-parts.ts +++ b/scripts/tasks/sandbox-parts.ts @@ -65,7 +65,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, From a89b141982d91310e1af6395149c9132b7a0d886 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Fri, 12 Jan 2024 09:36:03 +0100 Subject: [PATCH 05/13] add back deprecated flags --- code/lib/cli/src/generate.ts | 5 + code/lib/cli/src/upgrade.ts | 185 ++++++++++++++++++++++++++++++++++- code/lib/cli/src/versions.ts | 1 + 3 files changed, 186 insertions(+), 5 deletions(-) diff --git a/code/lib/cli/src/generate.ts b/code/lib/cli/src/generate.ts index be3c4da1175e..4693a065eed5 100644 --- a/code/lib/cli/src/generate.ts +++ b/code/lib/cli/src/generate.ts @@ -81,6 +81,11 @@ command('upgrade') .option('-N --use-npm', 'Use NPM to install dependencies (deprecated)') .option('-y --yes', 'Skip prompting the user') .option('-n --dry-run', 'Only check for upgrades, do not install') + .option( + '-t --tag ', + 'Upgrade to a certain npm dist-tag (e.g. next, prerelease) (deprecated)' + ) + .option('-p --prerelease', 'Upgrade to the pre-release packages (deprecated)') .option('-s --skip-check', 'Skip postinstall version and automigration checks') .option('-c, --config-dir ', 'Directory where to load Storybook configurations from') .action(async (options: UpgradeOptions) => upgrade(options).catch(() => process.exit(1))); diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts index 787998fce0a8..315290b30d00 100644 --- a/code/lib/cli/src/upgrade.ts +++ b/code/lib/cli/src/upgrade.ts @@ -1,7 +1,7 @@ import { sync as spawnSync } from 'cross-spawn'; import { telemetry, getStorybookCoreVersion } from '@storybook/telemetry'; -import semver, { coerce, eq, lt, prerelease } from 'semver'; -import { logger } from '@storybook/node-logger'; +import semver, { coerce, eq, lt } from 'semver'; +import { deprecate, logger } from '@storybook/node-logger'; import { withTelemetry } from '@storybook/core-server'; import { UpgradeStorybookToLowerVersionError, @@ -11,8 +11,8 @@ import { import chalk from 'chalk'; import dedent from 'ts-dedent'; import boxen from 'boxen'; -import type { PackageManagerName } from './js-package-manager'; -import { JsPackageManagerFactory, useNpmWarning } from './js-package-manager'; +import type { PackageJsonWithMaybeDeps, PackageManagerName } from './js-package-manager'; +import { JsPackageManagerFactory, getPackageDetails, useNpmWarning } from './js-package-manager'; import { commandLog } from './helpers'; import { automigrate } from './automigrate'; import { isCorePackage } from './utils'; @@ -91,7 +91,165 @@ export const checkVersionConsistency = () => { }); }; +/** + * DEPRECATED BEHAVIOR SECTION + */ + +type ExtraFlags = Record; +const EXTRA_FLAGS: ExtraFlags = { + 'react-scripts@<5': ['--reject', '/preset-create-react-app/'], +}; + +export const addExtraFlags = ( + extraFlags: ExtraFlags, + flags: string[], + { dependencies, devDependencies }: PackageJsonWithMaybeDeps +) => { + return Object.entries(extraFlags).reduce( + (acc, entry) => { + const [pattern, extra] = entry; + const [pkg, specifier] = getPackageDetails(pattern); + const pkgVersion = dependencies[pkg] || devDependencies[pkg]; + + if (pkgVersion && semver.satisfies(semver.coerce(pkgVersion), specifier)) { + return [...acc, ...extra]; + } + + return acc; + }, + [...flags] + ); +}; + +export const addNxPackagesToReject = (flags: string[]) => { + const newFlags = [...flags]; + const index = flags.indexOf('--reject'); + if (index > -1) { + // Try to understand if it's in the format of a regex pattern + if (newFlags[index + 1].endsWith('/') && newFlags[index + 1].startsWith('/')) { + // Remove last and first slash so that I can add the parentheses + newFlags[index + 1] = newFlags[index + 1].substring(1, newFlags[index + 1].length - 1); + newFlags[index + 1] = `/(${newFlags[index + 1]}|@nrwl/storybook|@nx/storybook)/`; + } else { + // Adding the two packages as comma-separated values + // If the existing rejects are in regex format, they will be ignored. + // Maybe we need to find a more robust way to treat rejects? + newFlags[index + 1] = `${newFlags[index + 1]},@nrwl/storybook,@nx/storybook`; + } + } else { + newFlags.push('--reject'); + newFlags.push('@nrwl/storybook,@nx/storybook'); + } + return newFlags; +}; + +export const deprecatedUpgrade = async ({ + tag, + prerelease, + skipCheck, + useNpm, + packageManager: pkgMgr, + dryRun, + configDir, + yes, + ...options +}: UpgradeOptions) => { + if (useNpm) { + useNpmWarning(); + // eslint-disable-next-line no-param-reassign + pkgMgr = 'npm'; + } + if (tag) { + deprecate( + 'The --tag flag is deprecated. Specify the version you want to upgrade to with `npx storybook@ upgrade` instead' + ); + } else if (prerelease) { + deprecate( + 'The --prerelease flag is deprecated. Specify the version you want to upgrade to with `npx storybook@ upgrade` instead' + ); + } + const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); + + const beforeVersion = await getStorybookCoreVersion(); + + commandLog(`Checking for latest versions of '@storybook/*' packages`); + + if (tag && prerelease) { + throw new Error( + `Cannot set both --tag and --prerelease. Use --tag next to get the latest prereleae` + ); + } + + let target = 'latest'; + if (prerelease) { + // '@next' is storybook's convention for the latest prerelease tag. + // This used to be 'greatest', but that was not reliable and could pick canaries, etc. + // and random releases of other packages with storybook in their name. + target = '@next'; + } else if (tag) { + target = `@${tag}`; + } + + let flags = []; + if (!dryRun) flags.push('--upgrade'); + flags.push('--target'); + flags.push(target); + flags = addExtraFlags(EXTRA_FLAGS, flags, await packageManager.retrievePackageJson()); + flags = addNxPackagesToReject(flags); + const check = spawnSync('npx', ['npm-check-updates@latest', '/storybook/', ...flags], { + stdio: 'pipe', + shell: true, + }); + logger.info(check.stdout.toString()); + logger.info(check.stderr.toString()); + + const checkSb = spawnSync('npx', ['npm-check-updates@latest', 'sb', ...flags], { + stdio: 'pipe', + shell: true, + }); + logger.info(checkSb.stdout.toString()); + logger.info(checkSb.stderr.toString()); + + if (!dryRun) { + commandLog(`Installing upgrades`); + await packageManager.installDependencies(); + } + + let automigrationResults; + if (!skipCheck) { + checkVersionConsistency(); + automigrationResults = await automigrate({ dryRun, yes, packageManager: pkgMgr, configDir }); + } + if (!options.disableTelemetry) { + const afterVersion = await getStorybookCoreVersion(); + const { preCheckFailure, fixResults } = automigrationResults || {}; + const automigrationTelemetry = { + automigrationResults: preCheckFailure ? null : fixResults, + automigrationPreCheckFailure: preCheckFailure || null, + }; + telemetry('upgrade', { + prerelease, + tag, + beforeVersion, + afterVersion, + ...automigrationTelemetry, + }); + } +}; + +/** + * DEPRECATED BEHAVIOR SECTION END + */ + export interface UpgradeOptions { + /** + * @deprecated + */ + tag: string; + /** + * @deprecated + */ + prerelease: boolean; skipCheck: boolean; useNpm: boolean; packageManager: PackageManagerName; @@ -102,6 +260,8 @@ export interface UpgradeOptions { } export const doUpgrade = async ({ + tag, + prerelease, skipCheck, useNpm, packageManager: pkgMgr, @@ -115,6 +275,21 @@ export const doUpgrade = async ({ // eslint-disable-next-line no-param-reassign pkgMgr = 'npm'; } + if (tag || prerelease) { + await deprecatedUpgrade({ + tag, + prerelease, + skipCheck, + useNpm, + packageManager: pkgMgr, + dryRun, + configDir, + yes, + ...options, + }); + return; + } + const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); const currentVersion = versions['@storybook/cli']; @@ -129,7 +304,7 @@ export const doUpgrade = async ({ const latestVersion = await packageManager.latestVersion('@storybook/cli'); const isOutdated = lt(currentVersion, latestVersion); - const isPrerelease = prerelease(currentVersion) !== null; + const isPrerelease = semver.prerelease(currentVersion) !== null; const borderColor = isOutdated ? '#FC521F' : '#F1618C'; diff --git a/code/lib/cli/src/versions.ts b/code/lib/cli/src/versions.ts index c60d37df1f4f..2d44ce1d69ba 100644 --- a/code/lib/cli/src/versions.ts +++ b/code/lib/cli/src/versions.ts @@ -81,6 +81,7 @@ export default { '@storybook/svelte-webpack5': '7.6.7', '@storybook/sveltekit': '7.6.7', '@storybook/telemetry': '7.6.7', + '@storybook/test': '7.6.7', '@storybook/theming': '7.6.7', '@storybook/types': '7.6.7', '@storybook/vue': '7.6.7', From 2ac5b32b642ca993cce1d07a9c4c7bd8f8f8a9f3 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Fri, 12 Jan 2024 10:02:22 +0100 Subject: [PATCH 06/13] fix automigrations and readmes --- code/frameworks/nextjs/README.md | 2 +- code/frameworks/preact-vite/README.md | 2 +- code/frameworks/sveltekit/README.md | 3 +-- code/lib/cli/src/automigrate/fixes/builder-vite.ts | 6 +++++- .../lib/cli/src/automigrate/helpers/checkWebpack5Builder.ts | 6 ++++-- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/code/frameworks/nextjs/README.md b/code/frameworks/nextjs/README.md index 252fb329167d..b1333b8cf315 100644 --- a/code/frameworks/nextjs/README.md +++ b/code/frameworks/nextjs/README.md @@ -98,7 +98,7 @@ npx storybook@latest init This framework is designed to work with Storybook 7. If you’re not already using v7, upgrade with this command: ```bash -npx storybook@latest upgrade --prerelease +npx storybook@latest upgrade ``` #### Automatic migration diff --git a/code/frameworks/preact-vite/README.md b/code/frameworks/preact-vite/README.md index e418166a3b54..1e7d742e1674 100644 --- a/code/frameworks/preact-vite/README.md +++ b/code/frameworks/preact-vite/README.md @@ -22,7 +22,7 @@ npx storybook@latest init This framework is designed to work with Storybook 7. If you’re not already using v7, upgrade with this command: ```bash -npx storybook@latest upgrade --prerelease +npx storybook@latest upgrade ``` #### Manual migration diff --git a/code/frameworks/sveltekit/README.md b/code/frameworks/sveltekit/README.md index 3e4b96f8f83b..78d6f61eab5b 100644 --- a/code/frameworks/sveltekit/README.md +++ b/code/frameworks/sveltekit/README.md @@ -17,7 +17,6 @@ Check out our [Frameworks API](https://storybook.js.org/blog/framework-api/) ann - [Mocking links](#mocking-links) - [Troubleshooting](#troubleshooting) - [Error: `ERR! SyntaxError: Identifier '__esbuild_register_import_meta_url__' has already been declared` when starting Storybook](#error-err-syntaxerror-identifier-__esbuild_register_import_meta_url__-has-already-been-declared-when-starting-storybook) - - [Error: `Cannot read properties of undefined (reading 'disable_scroll_handling')` in preview](#error-cannot-read-properties-of-undefined-reading-disable_scroll_handling-in-preview) - [Acknowledgements](#acknowledgements) ## Supported features @@ -64,7 +63,7 @@ npx storybook@latest init This framework is designed to work with Storybook 7. If you’re not already using v7, upgrade with this command: ```bash -npx storybook@latest upgrade --prerelease +npx storybook@latest upgrade ``` #### Automatic migration diff --git a/code/lib/cli/src/automigrate/fixes/builder-vite.ts b/code/lib/cli/src/automigrate/fixes/builder-vite.ts index b7ee1317957c..b81102d20d2c 100644 --- a/code/lib/cli/src/automigrate/fixes/builder-vite.ts +++ b/code/lib/cli/src/automigrate/fixes/builder-vite.ts @@ -6,6 +6,7 @@ import { writeConfig } from '@storybook/csf-tools'; import type { Fix } from '../types'; import type { PackageJson } from '../../js-package-manager'; import { updateMainConfig } from '../helpers/mainConfigFile'; +import { getStorybookVersionSpecifier } from '../../helpers'; const logger = console; @@ -68,8 +69,11 @@ export const builderVite: Fix = { logger.info(`✅ Adding '@storybook/builder-vite' as dev dependency`); if (!dryRun) { + const versionToInstall = getStorybookVersionSpecifier( + await packageManager.retrievePackageJson() + ); await packageManager.addDependencies({ installAsDevDependencies: true }, [ - '@storybook/builder-vite', + `@storybook/builder-vite@${versionToInstall}`, ]); } diff --git a/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.ts b/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.ts index 8d59d62d0bcc..10186ac40430 100644 --- a/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.ts +++ b/code/lib/cli/src/automigrate/helpers/checkWebpack5Builder.ts @@ -20,9 +20,11 @@ export const checkWebpack5Builder = async ({ To upgrade to the latest stable release, run this from your project directory: - ${chalk.cyan('npx storybook upgrade')} + ${chalk.cyan('npx storybook@latest upgrade')} - Add the ${chalk.cyan('--prerelease')} flag to get the latest prerelease. + To upgrade to the latest pre-release, run this from your project directory: + + ${chalk.cyan('npx storybook@next upgrade')} `.trim() ); return null; From 9cb3fe601077afdb497b06a38de0ffd72fe262b2 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Fri, 12 Jan 2024 10:33:58 +0100 Subject: [PATCH 07/13] update message matches new upgrade API --- code/lib/core-server/src/utils/update-check.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/code/lib/core-server/src/utils/update-check.ts b/code/lib/core-server/src/utils/update-check.ts index 2012921921a0..1ddb4cf85223 100644 --- a/code/lib/core-server/src/utils/update-check.ts +++ b/code/lib/core-server/src/utils/update-check.ts @@ -38,8 +38,7 @@ export function createUpdateMessage(updateInfo: VersionCheck, version: string): try { const isPrerelease = semver.prerelease(updateInfo.data.latest.version); - const suffix = isPrerelease ? '@next upgrade --prerelease' : '@latest upgrade'; - const upgradeCommand = `npx storybook${suffix}`; + const upgradeCommand = `npx storybook@${isPrerelease ? 'next' : 'latest'} upgrade`; updateMessage = updateInfo.success && semver.lt(version, updateInfo.data.latest.version) ? dedent` From 1d7f277bc1989126bafd945a2e424d531dadba9f Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Fri, 12 Jan 2024 14:57:25 +0100 Subject: [PATCH 08/13] maybe fix --- code/package.json | 1 + code/yarn.lock | 24 ------------------------ node_modules/.yarn-state.yml | 2 +- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/code/package.json b/code/package.json index bc99df078e42..498f40a94649 100644 --- a/code/package.json +++ b/code/package.json @@ -86,6 +86,7 @@ "esbuild": "^0.18.0", "eslint": "^8.28.0", "playwright": "1.36.0", + "@storybook/theming": "workspace:*", "playwright-core": "1.36.0", "serialize-javascript": "^3.1.0", "type-fest": "~2.19" diff --git a/code/yarn.lock b/code/yarn.lock index 68700a300f6a..58e417e70fca 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -6699,15 +6699,6 @@ __metadata: languageName: unknown linkType: soft -"@storybook/client-logger@npm:7.5.0": - version: 7.5.0 - resolution: "@storybook/client-logger@npm:7.5.0" - dependencies: - "@storybook/global": "npm:^5.0.0" - checksum: 90326c49a224bf21680c04ffee94725bf75658086093ccb839a8aae39476929c4719eafb18e498a148cf0dd956d4e9a5d3b2a34d09ca4fd25e2af553458558ac - languageName: node - linkType: hard - "@storybook/client-logger@workspace:*, @storybook/client-logger@workspace:lib/client-logger": version: 0.0.0-use.local resolution: "@storybook/client-logger@workspace:lib/client-logger" @@ -8141,21 +8132,6 @@ __metadata: languageName: node linkType: hard -"@storybook/theming@npm:^7.0.2": - version: 7.5.0 - resolution: "@storybook/theming@npm:7.5.0" - dependencies: - "@emotion/use-insertion-effect-with-fallbacks": "npm:^1.0.0" - "@storybook/client-logger": "npm:7.5.0" - "@storybook/global": "npm:^5.0.0" - memoizerific: "npm:^1.11.3" - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 57da8e27c748cbec4dc1661cdd2d449949d97476d8e97933696b31d07c7361cbbcca8d7225cc00ca078daa160023b8965ddec7c23519ce0a4ef2658246b062e7 - languageName: node - linkType: hard - "@storybook/theming@workspace:*, @storybook/theming@workspace:lib/theming": version: 0.0.0-use.local resolution: "@storybook/theming@workspace:lib/theming" diff --git a/node_modules/.yarn-state.yml b/node_modules/.yarn-state.yml index af981fec0a05..a6b1a224e847 100644 --- a/node_modules/.yarn-state.yml +++ b/node_modules/.yarn-state.yml @@ -7,4 +7,4 @@ __metadata: "@storybook/root@workspace:.": locations: - - "" \ No newline at end of file + - "" From 9f449a5bce2b6bc7ef564b657f706a1089775d45 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Fri, 12 Jan 2024 17:58:42 +0100 Subject: [PATCH 09/13] fix tests --- code/lib/cli/src/upgrade.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/lib/cli/src/upgrade.test.ts b/code/lib/cli/src/upgrade.test.ts index d9445b66fcfb..56827546b548 100644 --- a/code/lib/cli/src/upgrade.test.ts +++ b/code/lib/cli/src/upgrade.test.ts @@ -9,7 +9,7 @@ jest.mock('@storybook/telemetry'); jest.mock('./versions', () => { const originalVersions = jest.requireActual('./versions').default; return { - default: Object.keys(originalVersions).reduce((acc, key) => { + ...Object.keys(originalVersions).reduce((acc, key) => { acc[key] = '8.0.0'; return acc; }, {} as Record), From 24f72ed993a66f857ca0f2f568a50bffd61511bb Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Mon, 15 Jan 2024 12:56:09 +0100 Subject: [PATCH 10/13] fix the call to init in the cwd & ignore if next tag is missing --- code/lib/cli/src/sandbox.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/code/lib/cli/src/sandbox.ts b/code/lib/cli/src/sandbox.ts index 0d8003e07bac..2f0a8859a646 100644 --- a/code/lib/cli/src/sandbox.ts +++ b/code/lib/cli/src/sandbox.ts @@ -41,7 +41,10 @@ export const sandbox = async ( force: pkgMgr, }); const latestVersion = await packageManager.latestVersion('@storybook/cli'); - const nextVersion = await packageManager.latestVersion('@storybook/cli@next'); + // In verdaccio we often only have the latest tag, so this will fail. + const nextVersion = await packageManager + .latestVersion('@storybook/cli@next') + .catch((e) => '0.0.0'); const currentVersion = versions['@storybook/cli']; const isPrerelease = prerelease(currentVersion); const isOutdated = lt(currentVersion, isPrerelease ? nextVersion : latestVersion); @@ -215,12 +218,15 @@ export const sandbox = async ( // 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 + const before = process.cwd(); + process.chdir(templateDestination); await doInitiate( { ...options, }, pkg ); + process.chdir(before); } } catch (err) { logger.error(`🚨 Failed to download sandbox template: ${err.message}`); From a2055b334c72ca4885f311f2cf9bcb5c9070beb2 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Mon, 15 Jan 2024 15:56:53 +0100 Subject: [PATCH 11/13] Merge pull request #25596 from storybookjs/jeppe/versioned-canary-sandbox CLI: Support upgrading to canary versions (cherry picked from commit f0913477c2bf3e32c0d61445bd3bf5b5559df45a) --- .github/workflows/canary-release-pr.yml | 10 +++++----- code/lib/cli/src/upgrade.ts | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.github/workflows/canary-release-pr.yml b/.github/workflows/canary-release-pr.yml index 827f24a2c5f8..659765318fe5 100644 --- a/.github/workflows/canary-release-pr.yml +++ b/.github/workflows/canary-release-pr.yml @@ -1,11 +1,11 @@ name: Publish canary release of PR -run-name: 'Canary release: PR #${{ inputs.pr }}, triggered by ${{ github.triggering_actor }}' +run-name: "Canary release: PR #${{ inputs.pr }}, triggered by ${{ github.triggering_actor }}" on: workflow_dispatch: inputs: pr: - description: 'Pull request number to create a canary release for' + description: "Pull request number to create a canary release for" required: true type: number @@ -58,7 +58,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v3 with: - node-version-file: '.nvmrc' + node-version-file: ".nvmrc" - name: Cache dependencies uses: actions/cache@v3 with: @@ -91,10 +91,10 @@ jobs: with: githubToken: ${{ secrets.GH_TOKEN }} prNumber: ${{ inputs.pr }} - find: 'CANARY_RELEASE_SECTION' + find: "CANARY_RELEASE_SECTION" isHtmlCommentTag: true replace: | - This pull request has been released as version [`${{ steps.version.outputs.next-version }}`](https://npmjs.com/package/@storybook/cli/v/${{ steps.version.outputs.next-version }}). Install it by pinning all your Storybook dependencies to that version. + This pull request has been released as version `${{ steps.version.outputs.next-version }}`. Try it out in a new sandbox by running `npx storybook@${{ steps.version.outputs.next-version }} sandbox` or in an existing project with `npx storybook@${{ steps.version.outputs.next-version }} upgrade`.
More information diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts index 315290b30d00..eaee3a819f88 100644 --- a/code/lib/cli/src/upgrade.ts +++ b/code/lib/cli/src/upgrade.ts @@ -292,13 +292,14 @@ export const doUpgrade = async ({ const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); - const currentVersion = versions['@storybook/cli']; const beforeVersion = await getStorybookCoreVersion(); + const currentVersion = versions['@storybook/cli']; + const isCanary = currentVersion.startsWith('0.0.0'); - if (lt(currentVersion, beforeVersion)) { + if (!isCanary && lt(currentVersion, beforeVersion)) { throw new UpgradeStorybookToLowerVersionError({ beforeVersion, currentVersion }); } - if (eq(currentVersion, beforeVersion)) { + if (!isCanary && eq(currentVersion, beforeVersion)) { throw new UpgradeStorybookToSameVersionError({ beforeVersion }); } @@ -348,12 +349,12 @@ export const doUpgrade = async ({ // only upgrade packages that are in the monorepo return dependency in versions; }) as Array; - return monorepoDependencies.map( - (dependency) => - // add ^ modifier to the version if this is the latest and stable version - // example output: @storybook/react@^8.0.0 - `${dependency}@${!isOutdated || isPrerelease ? '^' : ''}${versions[dependency]}` - ); + return monorepoDependencies.map((dependency) => { + /* add ^ modifier to the version if this is the latest stable or prerelease version + example outputs: @storybook/react@^8.0.0 */ + const maybeCaret = (!isOutdated || isPrerelease) && !isCanary ? '^' : ''; + return `${dependency}@${maybeCaret}${versions[dependency]}`; + }); }; const upgradedDependencies = toUpgradedDependencies(packageJson.dependencies); From 838d2676c20b6d3798eab6048ab9274138078892 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Mon, 15 Jan 2024 16:43:52 +0100 Subject: [PATCH 12/13] init when download-dir = before-storybook --- code/lib/cli/src/sandbox.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/lib/cli/src/sandbox.ts b/code/lib/cli/src/sandbox.ts index 2f0a8859a646..17dd4763e87e 100644 --- a/code/lib/cli/src/sandbox.ts +++ b/code/lib/cli/src/sandbox.ts @@ -216,7 +216,7 @@ export const sandbox = async ( // 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) { + if (downloadType === 'before-storybook' && init) { // we run doInitiate, instead of initiate, to avoid sending this init event to telemetry, because it's not a real world project const before = process.cwd(); process.chdir(templateDestination); From 5c11b71a056af96b5fca03c9f4348786adf478d8 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 16 Jan 2024 15:14:44 +0100 Subject: [PATCH 13/13] fixes --- code/lib/cli/src/generators/REACT_SCRIPTS/index.ts | 7 +------ code/lib/cli/src/versions.ts | 1 + 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/code/lib/cli/src/generators/REACT_SCRIPTS/index.ts b/code/lib/cli/src/generators/REACT_SCRIPTS/index.ts index 2177a9d5090e..2bdd2dd8c706 100644 --- a/code/lib/cli/src/generators/REACT_SCRIPTS/index.ts +++ b/code/lib/cli/src/generators/REACT_SCRIPTS/index.ts @@ -6,7 +6,6 @@ import dedent from 'ts-dedent'; import { baseGenerator } from '../baseGenerator'; import type { Generator } from '../types'; import { CoreBuilder } from '../../project_types'; -import versions from '../../versions'; const generator: Generator = async (packageManager, npmOptions, options) => { const monorepoRootPath = path.join(__dirname, '..', '..', '..', '..', '..', '..'); @@ -47,11 +46,7 @@ const generator: Generator = async (packageManager, npmOptions, options) => { // Miscellaneous dependency to add to be sure Storybook + CRA is working fine with Yarn PnP mode extraPackages.push('prop-types'); - const version = versions['@storybook/preset-create-react-app']; - const extraAddons = [ - `@storybook/preset-create-react-app@${version}`, - '@storybook/addon-onboarding', - ]; + const extraAddons = [`@storybook/preset-create-react-app`, '@storybook/addon-onboarding@^1.0.0']; await baseGenerator( packageManager, diff --git a/code/lib/cli/src/versions.ts b/code/lib/cli/src/versions.ts index 4679c3054d05..1b5da6ec1e8e 100644 --- a/code/lib/cli/src/versions.ts +++ b/code/lib/cli/src/versions.ts @@ -83,6 +83,7 @@ export default { '@storybook/telemetry': '7.6.8', '@storybook/theming': '7.6.8', '@storybook/types': '7.6.8', + '@storybook/test': '7.6.8', '@storybook/vue': '7.6.8', '@storybook/vue-vite': '7.6.8', '@storybook/vue-webpack5': '7.6.8',