From 923deb76b1e18352562f8183492c539ec28de943 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 7 Feb 2024 00:14:26 +0100 Subject: [PATCH 01/25] refactor cli to add autoblockers --- .../autoblock/block-react-script-version.ts | 26 +++ .../cli/src/autoblock/block-stories-mdx.ts | 31 ++++ .../cli/src/autoblock/block-storystorev6.ts | 40 ++++ code/lib/cli/src/autoblock/index.ts | 73 ++++++++ code/lib/cli/src/autoblock/types.ts | 42 +++++ code/lib/cli/src/automigrate/index.ts | 92 ++-------- code/lib/cli/src/automigrate/types.ts | 10 +- code/lib/cli/src/upgrade.ts | 173 +++++++++++++----- .../js-package-manager/JsPackageManager.ts | 13 +- 9 files changed, 372 insertions(+), 128 deletions(-) create mode 100644 code/lib/cli/src/autoblock/block-react-script-version.ts create mode 100644 code/lib/cli/src/autoblock/block-stories-mdx.ts create mode 100644 code/lib/cli/src/autoblock/block-storystorev6.ts create mode 100644 code/lib/cli/src/autoblock/index.ts create mode 100644 code/lib/cli/src/autoblock/types.ts diff --git a/code/lib/cli/src/autoblock/block-react-script-version.ts b/code/lib/cli/src/autoblock/block-react-script-version.ts new file mode 100644 index 000000000000..ad656242456b --- /dev/null +++ b/code/lib/cli/src/autoblock/block-react-script-version.ts @@ -0,0 +1,26 @@ +import { createBlocker } from './types'; +import { dedent } from 'ts-dedent'; +import { gte } from 'semver'; + +export const blocker = createBlocker({ + id: 'storiesMdxUsage', + async check({ packageManager }) { + const version = await packageManager.getVersion('react-scripts'); + if (version && gte(version, '5.0.0')) { + return false; + } + return { version }; + }, + message(options, data) { + return `Found react-script version: ${data.version}, please upgrade to latest.`; + }, + log() { + return dedent` + Support react-script < 5.0.0 has been removed. + Please see the migration guide for more information: + https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#create-react-app-dropped-cra4-support + + Upgrade to the latest version of react-scripts. + `; + }, +}); diff --git a/code/lib/cli/src/autoblock/block-stories-mdx.ts b/code/lib/cli/src/autoblock/block-stories-mdx.ts new file mode 100644 index 000000000000..ff726cada631 --- /dev/null +++ b/code/lib/cli/src/autoblock/block-stories-mdx.ts @@ -0,0 +1,31 @@ +import { createBlocker } from './types'; +import { dedent } from 'ts-dedent'; +import { glob } from 'glob'; + +export const blocker = createBlocker({ + id: 'storiesMdxUsage', + async check() { + const files = await glob('**/*.stories.mdx', { cwd: process.cwd() }); + if (files.length > 0) { + return false; + } + return { files }; + }, + message(options, data) { + return `Found ${data.files.length} stories.mdx files, these must be migrated.`; + }, + log() { + return dedent` + Support for *.stories.mdx files has been removed. + Please see the migration guide for more information: + https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#dropping-support-for-storiesmdx-csf-in-mdx-format-and-mdx1-support + + Storybook will also require you to use MDX 3.0.0 or later. + Check the migration guide for more information: + https://mdxjs.com/blog/v3/ + + Manually run the migration script to convert your stories.mdx files to CSF format documented here: + https://storybook.js.org/docs/migration-guide#storiesmdx-to-mdxcsf + `; + }, +}); diff --git a/code/lib/cli/src/autoblock/block-storystorev6.ts b/code/lib/cli/src/autoblock/block-storystorev6.ts new file mode 100644 index 000000000000..e00982c34d92 --- /dev/null +++ b/code/lib/cli/src/autoblock/block-storystorev6.ts @@ -0,0 +1,40 @@ +import { relative } from 'path'; +import { createBlocker } from './types'; +import { dedent } from 'ts-dedent'; +import type { StorybookConfigRaw } from 'lib/types/src'; + +export const blocker = createBlocker({ + id: 'storyStoreV7removal', + async check({ mainConfig }) { + const features = (mainConfig as any as StorybookConfigRaw)?.features; + if (features === undefined) { + return false; + } + if (Object.hasOwn(features, 'storyStoreV7')) { + return true; + } + return false; + }, + message(options, data) { + const mainConfigPath = relative(process.cwd(), options.mainConfigPath); + return `StoryStoreV7 feature most be removed from ${mainConfigPath}`; + }, + log() { + return dedent` + StoryStoreV7 feature most be removed from your Storybook configuration. + This feature was removed in Storybook 7.0.0. + Please see the migration guide for more information: + https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#story-store-v7 + + In your Storybook configuration file you have this code: + + module.exports = { + features: { + storyStoreV7: false, <--- remove this line + }, + }; + + You need to remove the storyStoreV7 property. + `; + }, +}); diff --git a/code/lib/cli/src/autoblock/index.ts b/code/lib/cli/src/autoblock/index.ts new file mode 100644 index 000000000000..292c3e146638 --- /dev/null +++ b/code/lib/cli/src/autoblock/index.ts @@ -0,0 +1,73 @@ +import type { AutoblockOptions } from './types'; +import { logger } from '@storybook/node-logger'; +import chalk from 'chalk'; +import boxen from 'boxen'; +import { writeFile } from 'fs/promises'; + +const excludesFalse = (x: T | false): x is T => x !== false; + +const blockers = [ + // add/remove blockers here + import('./block-storystorev6'), +]; + +export const autoblock = async (options: AutoblockOptions) => { + logger.info('Checking for upgrade blockers...'); + + const out = await Promise.all( + blockers.map(async (i) => { + const { blocker } = await i; + const result = await blocker.check(options); + if (result) { + return { + id: blocker.id, + value: true, + message: blocker.message(options, result), + log: blocker.log(options, result), + }; + } else { + return false; + } + }) + ); + + const faults = out.filter(excludesFalse); + + if (faults.length > 0) { + const LOG_FILE_NAME = 'migration-storybook.log'; + + const messages = { + welcome: `Blocking your upgrade because of the following issues:`, + reminder: chalk.yellow('Fix the above issues and try running the upgrade command again.'), + logfile: chalk.yellow(`You can find more details in ./${LOG_FILE_NAME}.`), + }; + const borderColor = '#FC521F'; + + logger.plain('Oh no..'); + logger.plain( + boxen( + [messages.welcome] + .concat(faults.map((i) => i.message)) + .concat([messages.reminder]) + .concat([messages.logfile]) + .join('\n\n'), + { borderStyle: 'round', padding: 1, borderColor } + ) + ); + + await writeFile( + LOG_FILE_NAME, + faults.map((i) => '(' + i.id + '):\n' + i.log).join('\n\n----\n\n'), + { + encoding: 'utf-8', + } + ); + + return faults[0].id; + } + + logger.plain('No blockers found.'); + logger.line(); + + return null; +}; diff --git a/code/lib/cli/src/autoblock/types.ts b/code/lib/cli/src/autoblock/types.ts new file mode 100644 index 000000000000..0a59f6ec97f4 --- /dev/null +++ b/code/lib/cli/src/autoblock/types.ts @@ -0,0 +1,42 @@ +import type { JsPackageManager, PackageJson } from '@storybook/core-common'; +import type { StorybookConfig } from '@storybook/types'; + +export interface AutoblockOptions { + packageManager: JsPackageManager; + packageJson: PackageJson; + mainConfig: StorybookConfig; + mainConfigPath: string; + configDir: string; +} + +export interface Blocker { + /** + * A unique string to identify the blocker with. + */ + id: string; + /** + * Check if the blocker should block. + * + * @param {AutoblockOptions} options - The context. + * @returns {Promise} - Return a truthy value to activate the block, return false to proceed. + */ + check: (options: AutoblockOptions) => Promise; + /** + * Format a message to be printed to the log-file. + * @param {AutoblockOptions} options - The context. + * @param {T} data - The data returned from the check method. + * @returns {string} - The string to print to the terminal. + */ + message: (options: AutoblockOptions, data: T) => string; + /** + * Format a message to be printed to the log-file. + * @param {AutoblockOptions} options - The context. + * @param {T} data - The data returned from the check method. + * @returns {string} - The string to print to the log-file. + */ + log: (options: AutoblockOptions, data: T) => string; +} + +export function createBlocker(block: Blocker) { + return block; +} diff --git a/code/lib/cli/src/automigrate/index.ts b/code/lib/cli/src/automigrate/index.ts index 3adeff5e0ead..f98d15c4f815 100644 --- a/code/lib/cli/src/automigrate/index.ts +++ b/code/lib/cli/src/automigrate/index.ts @@ -3,20 +3,13 @@ import chalk from 'chalk'; import boxen from 'boxen'; import { createWriteStream, move, remove } from 'fs-extra'; import tempy from 'tempy'; -import dedent from 'ts-dedent'; import { join } from 'path'; import invariant from 'tiny-invariant'; -import { - getStorybookInfo, - loadMainConfig, - getCoercedStorybookVersion, - JsPackageManagerFactory, -} from '@storybook/core-common'; -import type { PackageManagerName } from '@storybook/core-common'; +import type { JsPackageManager } from '@storybook/core-common'; -import type { Fix, FixId, FixOptions, FixSummary } from './fixes'; -import { FixStatus, PreCheckFailure, allFixes } from './fixes'; +import type { Fix, FixId, AutofixOptions, FixSummary, PreCheckFailure } from './fixes'; +import { FixStatus, allFixes } from './fixes'; import { cleanLog } from './helpers/cleanLog'; import { getMigrationSummary } from './helpers/getMigrationSummary'; import { getStorybookData } from './helpers/mainConfigFile'; @@ -57,13 +50,15 @@ export const automigrate = async ({ fixes: inputFixes, dryRun, yes, - packageManager: pkgMgr, + packageManager, list, - configDir: userSpecifiedConfigDir, + configDir, + mainConfigPath, + storybookVersion, renderer: rendererPackage, skipInstall, hideMigrationSummary = false, -}: FixOptions = {}): Promise<{ +}: AutofixOptions): Promise<{ fixResults: Record; preCheckFailure?: PreCheckFailure; } | null> => { @@ -87,10 +82,12 @@ export const automigrate = async ({ const { fixResults, fixSummary, preCheckFailure } = await runFixes({ fixes, - pkgMgr, - userSpecifiedConfigDir, + packageManager, rendererPackage, skipInstall, + configDir, + mainConfigPath, + storybookVersion, dryRun, yes, }); @@ -107,7 +104,6 @@ export const automigrate = async ({ } if (!hideMigrationSummary) { - const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); const installationMetadata = await packageManager.findInstallations([ '@storybook/*', 'storybook', @@ -129,78 +125,30 @@ export async function runFixes({ fixes, dryRun, yes, - pkgMgr, - userSpecifiedConfigDir, rendererPackage, skipInstall, + configDir, + packageManager, + mainConfigPath, + storybookVersion, }: { fixes: Fix[]; yes?: boolean; dryRun?: boolean; - pkgMgr?: PackageManagerName; - userSpecifiedConfigDir?: string; rendererPackage?: string; skipInstall?: boolean; + configDir: string; + packageManager: JsPackageManager; + mainConfigPath: string; + storybookVersion: string; }): Promise<{ preCheckFailure?: PreCheckFailure; fixResults: Record; fixSummary: FixSummary; }> { - const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); - const fixResults = {} as Record; const fixSummary: FixSummary = { succeeded: [], failed: {}, manual: [], skipped: [] }; - const { configDir: inferredConfigDir, mainConfig: mainConfigPath } = getStorybookInfo( - await packageManager.retrievePackageJson(), - userSpecifiedConfigDir - ); - - const storybookVersion = await getCoercedStorybookVersion(packageManager); - - if (!storybookVersion) { - logger.info(dedent` - [Storybook automigrate] ❌ Unable to determine storybook version so the automigrations will be skipped. - 🤔 Are you running automigrate from your project directory? Please specify your Storybook config directory with the --config-dir flag. - `); - return { - fixResults, - fixSummary, - preCheckFailure: PreCheckFailure.UNDETECTED_SB_VERSION, - }; - } - - const configDir = userSpecifiedConfigDir || inferredConfigDir || '.storybook'; - try { - await loadMainConfig({ configDir }); - } catch (err) { - const errMessage = String(err); - if (errMessage.includes('No configuration files have been found')) { - logger.info( - dedent`[Storybook automigrate] Could not find or evaluate your Storybook main.js config directory at ${chalk.blue( - configDir - )} so the automigrations will be skipped. You might be running this command in a monorepo or a non-standard project structure. If that is the case, please rerun this command by specifying the path to your Storybook config directory via the --config-dir option.` - ); - return { - fixResults, - fixSummary, - preCheckFailure: PreCheckFailure.MAINJS_NOT_FOUND, - }; - } - logger.info( - dedent`[Storybook automigrate] ❌ Failed trying to evaluate ${chalk.blue( - mainConfigPath - )} with the following error: ${errMessage}` - ); - logger.info('Please fix the error and try again.'); - - return { - fixResults, - fixSummary, - preCheckFailure: PreCheckFailure.MAINJS_EVALUATION, - }; - } - for (let i = 0; i < fixes.length; i += 1) { const f = fixes[i] as Fix; let result; diff --git a/code/lib/cli/src/automigrate/types.ts b/code/lib/cli/src/automigrate/types.ts index 0fa57c1fdfcc..e3d13686b96b 100644 --- a/code/lib/cli/src/automigrate/types.ts +++ b/code/lib/cli/src/automigrate/types.ts @@ -1,5 +1,5 @@ import type { StorybookConfigRaw } from '@storybook/types'; -import type { JsPackageManager, PackageManagerName } from '@storybook/core-common'; +import type { JsPackageManager } from '@storybook/core-common'; export interface CheckOptions { packageManager: JsPackageManager; @@ -35,17 +35,19 @@ export enum PreCheckFailure { MAINJS_EVALUATION = 'mainjs_evaluation_error', } -export interface FixOptions { +export interface AutofixOptions { fixId?: FixId; list?: boolean; fixes?: Fix[]; yes?: boolean; dryRun?: boolean; - packageManager?: PackageManagerName; - configDir?: string; + packageManager: JsPackageManager; + configDir: string; renderer?: string; skipInstall?: boolean; hideMigrationSummary?: boolean; + mainConfigPath: string; + storybookVersion: string; } export enum FixStatus { diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts index 1d13c5a7f0c2..5291d9c6bf06 100644 --- a/code/lib/cli/src/upgrade.ts +++ b/code/lib/cli/src/upgrade.ts @@ -13,13 +13,17 @@ import dedent from 'ts-dedent'; import boxen from 'boxen'; import type { JsPackageManager, PackageManagerName } from '@storybook/core-common'; import { - JsPackageManagerFactory, isCorePackage, versions, - commandLog, + getStorybookInfo, + getCoercedStorybookVersion, + loadMainConfig, + JsPackageManagerFactory, } from '@storybook/core-common'; import { coerceSemver } from './helpers'; -import { automigrate } from './automigrate'; +import { automigrate } from './automigrate/index'; +import { autoblock } from './autoblock/index'; +import { PreCheckFailure } from './automigrate/types'; type Package = { package: string; @@ -108,7 +112,7 @@ export const checkVersionConsistency = () => { export interface UpgradeOptions { skipCheck: boolean; - packageManager: PackageManagerName; + packageManager?: PackageManagerName; dryRun: boolean; yes: boolean; disableTelemetry: boolean; @@ -117,19 +121,22 @@ export interface UpgradeOptions { export const doUpgrade = async ({ skipCheck, - packageManager: pkgMgr, + packageManager: packageManagerName, dryRun, - configDir, + configDir: userSpecifiedConfigDir, yes, ...options }: UpgradeOptions) => { - const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr }); + const packageManager = JsPackageManagerFactory.getPackageManager({ force: packageManagerName }); // If we can't determine the existing version fallback to v0.0.0 to not block the upgrade const beforeVersion = (await getInstalledStorybookVersion(packageManager)) ?? '0.0.0'; const currentVersion = versions['@storybook/cli']; - const isCanary = currentVersion.startsWith('0.0.0'); + const isCanary = + currentVersion.startsWith('0.0.0') || + beforeVersion.startsWith('portal:') || + beforeVersion.startsWith('workspace:'); if (!isCanary && lt(currentVersion, beforeVersion)) { throw new UpgradeStorybookToLowerVersionError({ beforeVersion, currentVersion }); @@ -138,7 +145,13 @@ export const doUpgrade = async ({ throw new UpgradeStorybookToSameVersionError({ beforeVersion }); } - const latestVersion = await packageManager.latestVersion('@storybook/cli'); + const [latestVersion, packageJson, storybookVersion] = await Promise.all([ + // + packageManager.latestVersion('@storybook/cli'), + packageManager.retrievePackageJson(), + getCoercedStorybookVersion(packageManager), + ]); + const isOutdated = lt(currentVersion, latestVersion); const isPrerelease = prerelease(currentVersion) !== null; @@ -168,36 +181,79 @@ export const doUpgrade = async ({ ) ); - const packageJson = await packageManager.retrievePackageJson(); + let results; - 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(coerceSemver(reactScriptsVersion), '5.0.0')) { - return false; - } - } + const { configDir: inferredConfigDir, mainConfig: mainConfigPath } = getStorybookInfo( + packageJson, + userSpecifiedConfigDir + ); + const configDir = userSpecifiedConfigDir || inferredConfigDir || '.storybook'; + + let mainConfigLoadingError = ''; - // 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 stable or prerelease version - example outputs: @storybook/react@^8.0.0 */ - const maybeCaret = (!isOutdated || isPrerelease) && !isCanary ? '^' : ''; - return `${dependency}@${maybeCaret}${versions[dependency]}`; + const mainConfig = await loadMainConfig({ configDir }).catch((err) => { + mainConfigLoadingError = String(err); + return false; + }); + + // GUARDS + if (!storybookVersion) { + logger.info(missingStorybookVersionMessage()); + results = { preCheckFailure: PreCheckFailure.UNDETECTED_SB_VERSION }; + } else if ( + typeof mainConfigPath === 'undefined' || + mainConfigLoadingError.includes('No configuration files have been found') + ) { + logger.info(mainjsNotFoundMessage(configDir)); + results = { preCheckFailure: PreCheckFailure.MAINJS_NOT_FOUND }; + } else if (typeof mainConfig === 'boolean') { + logger.info(mainjsExecutionFailureMessage(mainConfigPath, mainConfigLoadingError)); + results = { preCheckFailure: PreCheckFailure.MAINJS_EVALUATION }; + } + + // BLOCKERS + if (!results && typeof mainConfig !== 'boolean' && typeof mainConfigPath !== 'undefined') { + const blockResult = await autoblock({ + packageManager, + configDir, + packageJson, + mainConfig, + mainConfigPath, }); - }; + if (blockResult) { + results = { preCheckFailure: blockResult }; + } + } - const upgradedDependencies = toUpgradedDependencies(packageJson.dependencies); - const upgradedDevDependencies = toUpgradedDependencies(packageJson.devDependencies); + // INSTALL UPDATED DEPENDENCIES + if (!dryRun && !results) { + 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(coerceSemver(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 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); + const upgradedDevDependencies = toUpgradedDependencies(packageJson.devDependencies); - if (!dryRun) { - commandLog(`Updating dependencies in ${chalk.cyan('package.json')}..`); - logger.plain(''); + logger.info(`Updating dependencies in ${chalk.cyan('package.json')}..`); if (upgradedDependencies.length > 0) { await packageManager.addDependencies( { installAsDevDependencies: false, skipInstall: true, packageJson }, @@ -213,26 +269,61 @@ export const doUpgrade = async ({ await packageManager.installDependencies(); } - let automigrationResults; - if (!skipCheck) { + // AUTOMIGRATIONS + if (!skipCheck && !results && mainConfigPath && storybookVersion) { checkVersionConsistency(); - automigrationResults = await automigrate({ dryRun, yes, packageManager: pkgMgr, configDir }); + results = await automigrate({ + dryRun, + yes, + packageManager, + configDir, + mainConfigPath, + storybookVersion, + }); } + + // TELEMETRY if (!options.disableTelemetry) { - const afterVersion = await getInstalledStorybookVersion(packageManager); - const { preCheckFailure, fixResults } = automigrationResults || {}; + const { preCheckFailure, fixResults } = results || {}; const automigrationTelemetry = { automigrationResults: preCheckFailure ? null : fixResults, automigrationPreCheckFailure: preCheckFailure || null, }; - telemetry('upgrade', { + + await telemetry('upgrade', { beforeVersion, - afterVersion, + afterVersion: currentVersion, ...automigrationTelemetry, }); } }; +function missingStorybookVersionMessage(): string { + return dedent` + [Storybook automigrate] ❌ Unable to determine storybook version so the automigrations will be skipped. + 🤔 Are you running automigrate from your project directory? Please specify your Storybook config directory with the --config-dir flag. + `; +} + +function mainjsExecutionFailureMessage( + mainConfigPath: string, + mainConfigLoadingError: string +): string { + return dedent` + [Storybook automigrate] ❌ Failed trying to evaluate ${chalk.blue( + mainConfigPath + )} with the following error: ${mainConfigLoadingError} + + Please fix the error and try again. + `; +} + +function mainjsNotFoundMessage(configDir: string): string { + return dedent`[Storybook automigrate] Could not find or evaluate your Storybook main.js config directory at ${chalk.blue( + configDir + )} so the automigrations will be skipped. You might be running this command in a monorepo or a non-standard project structure. If that is the case, please rerun this command by specifying the path to your Storybook config directory via the --config-dir option.`; +} + export async function upgrade(options: UpgradeOptions): Promise { await withTelemetry('upgrade', { cliOptions: options }, () => doUpgrade(options)); } diff --git a/code/lib/core-common/src/js-package-manager/JsPackageManager.ts b/code/lib/core-common/src/js-package-manager/JsPackageManager.ts index 923a06409968..9c386ae0861f 100644 --- a/code/lib/core-common/src/js-package-manager/JsPackageManager.ts +++ b/code/lib/core-common/src/js-package-manager/JsPackageManager.ts @@ -8,7 +8,6 @@ import fs from 'fs'; import dedent from 'ts-dedent'; import { readFile, writeFile, readFileSync } from 'fs-extra'; import invariant from 'tiny-invariant'; -import { commandLog } from '../utils/log'; import type { PackageJson, PackageJsonWithDepsAndDevDeps } from './PackageJson'; import storybookPackagesVersions from '../versions'; import type { InstallationMetadata } from './types'; @@ -128,21 +127,13 @@ export abstract class JsPackageManager { * Install dependencies listed in `package.json` */ public async installDependencies() { - let done = commandLog('Preparing to install dependencies'); - done(); - - logger.log(); - logger.log(); - - done = commandLog('Installing dependencies'); - + logger.log('Installing dependencies...'); logger.log(); try { await this.runInstall(); - done(); } catch (e) { - done('An error occurred while installing dependencies.'); + logger.error('An error occurred while installing dependencies.'); throw new HandledError(e); } } From aeac06751ffe2e0ab10c3db41ed9d1fdc1aea141 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 7 Feb 2024 13:15:14 +0100 Subject: [PATCH 02/25] add tests & fix some review comments --- .../cli/src/autoblock/block-storystorev6.ts | 8 +- code/lib/cli/src/autoblock/index.test.ts | 109 ++++++++++++++++++ code/lib/cli/src/autoblock/index.ts | 19 ++- code/lib/cli/src/upgrade.ts | 2 +- 4 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 code/lib/cli/src/autoblock/index.test.ts diff --git a/code/lib/cli/src/autoblock/block-storystorev6.ts b/code/lib/cli/src/autoblock/block-storystorev6.ts index e00982c34d92..a0cd3d15481e 100644 --- a/code/lib/cli/src/autoblock/block-storystorev6.ts +++ b/code/lib/cli/src/autoblock/block-storystorev6.ts @@ -1,7 +1,7 @@ import { relative } from 'path'; import { createBlocker } from './types'; import { dedent } from 'ts-dedent'; -import type { StorybookConfigRaw } from 'lib/types/src'; +import type { StorybookConfigRaw } from '@storybook/types'; export const blocker = createBlocker({ id: 'storyStoreV7removal', @@ -17,18 +17,18 @@ export const blocker = createBlocker({ }, message(options, data) { const mainConfigPath = relative(process.cwd(), options.mainConfigPath); - return `StoryStoreV7 feature most be removed from ${mainConfigPath}`; + return `StoryStoreV7 feature must be removed from ${mainConfigPath}`; }, log() { return dedent` - StoryStoreV7 feature most be removed from your Storybook configuration. + StoryStoreV7 feature must be removed from your Storybook configuration. This feature was removed in Storybook 7.0.0. Please see the migration guide for more information: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#story-store-v7 In your Storybook configuration file you have this code: - module.exports = { + export default = { features: { storyStoreV7: false, <--- remove this line }, diff --git a/code/lib/cli/src/autoblock/index.test.ts b/code/lib/cli/src/autoblock/index.test.ts new file mode 100644 index 000000000000..ce5fa3170411 --- /dev/null +++ b/code/lib/cli/src/autoblock/index.test.ts @@ -0,0 +1,109 @@ +import { expect, test, vi } from 'vitest'; +import { autoblock } from './index'; +import { JsPackageManagerFactory } from '@storybook/core-common'; +import { createBlocker } from './types'; +import { writeFile as writeFileRaw } from 'node:fs/promises'; +import { logger } from '@storybook/node-logger'; + +vi.mock('node:fs/promises', () => ({ + writeFile: vi.fn(), +})); +vi.mock('boxen', () => ({ + default: vi.fn((x) => x), +})); +vi.mock('@storybook/node-logger', () => ({ + logger: { + info: vi.fn(), + line: vi.fn(), + plain: vi.fn(), + }, +})); + +const writeFile = vi.mocked(writeFileRaw); + +const blockers = { + alwaysPass: createBlocker({ + id: 'alwaysPass', + check: async () => false, + message: () => 'Always pass', + log: () => 'Always pass', + }), + alwaysFail: createBlocker({ + id: 'alwaysFail', + check: async () => ({ bad: true }), + message: () => 'Always fail', + log: () => '...', + }), + alwaysFail2: createBlocker({ + id: 'alwaysFail2', + check: async () => ({ disaster: true }), + message: () => 'Always fail 2', + log: () => '...', + }), +} as const; + +const baseOptions: Parameters[0] = { + configDir: '.storybook', + mainConfig: { + stories: [], + }, + mainConfigPath: '.storybook/main.ts', + packageJson: { + dependencies: {}, + devDependencies: {}, + }, + packageManager: JsPackageManagerFactory.getPackageManager({ force: 'npm' }), +}; + +test('with empty list', async () => { + const result = await autoblock({ ...baseOptions }, []); + expect(result).toBe(null); + expect(logger.plain).not.toHaveBeenCalledWith(expect.stringContaining('No blockers found')); +}); + +test('all passing', async () => { + const result = await autoblock({ ...baseOptions }, [ + Promise.resolve({ blocker: blockers.alwaysPass }), + Promise.resolve({ blocker: blockers.alwaysPass }), + ]); + expect(result).toBe(null); + expect(logger.plain).toHaveBeenCalledWith(expect.stringContaining('No blockers found')); +}); + +test('1 fail', async () => { + const result = await autoblock({ ...baseOptions }, [ + Promise.resolve({ blocker: blockers.alwaysPass }), + Promise.resolve({ blocker: blockers.alwaysFail }), + ]); + expect(writeFile).toHaveBeenCalledWith( + expect.any(String), + expect.stringContaining('alwaysFail'), + expect.any(Object) + ); + expect(result).toBe('alwaysFail'); + expect(logger.plain).toHaveBeenCalledWith(expect.stringContaining('Oh no..')); + + expect(writeFile.mock.calls[0][1]).toMatchInlineSnapshot(` + "(alwaysFail): + ..." + `); +}); + +test('multiple fails', async () => { + const result = await autoblock({ ...baseOptions }, [ + Promise.resolve({ blocker: blockers.alwaysPass }), + Promise.resolve({ blocker: blockers.alwaysFail }), + Promise.resolve({ blocker: blockers.alwaysFail2 }), + ]); + expect(writeFile.mock.calls[0][1]).toMatchInlineSnapshot(` + "(alwaysFail): + ... + + ---- + + (alwaysFail2): + ..." + `); + + expect(result).toBe('alwaysFail'); +}); diff --git a/code/lib/cli/src/autoblock/index.ts b/code/lib/cli/src/autoblock/index.ts index 292c3e146638..e4eb85eb4820 100644 --- a/code/lib/cli/src/autoblock/index.ts +++ b/code/lib/cli/src/autoblock/index.ts @@ -1,21 +1,30 @@ -import type { AutoblockOptions } from './types'; +import type { AutoblockOptions, Blocker } from './types'; import { logger } from '@storybook/node-logger'; import chalk from 'chalk'; import boxen from 'boxen'; -import { writeFile } from 'fs/promises'; +import { writeFile } from 'node:fs/promises'; const excludesFalse = (x: T | false): x is T => x !== false; -const blockers = [ +const blockers: () => BlockerModule[] = () => [ // add/remove blockers here import('./block-storystorev6'), ]; -export const autoblock = async (options: AutoblockOptions) => { +type BlockerModule = Promise<{ blocker: Blocker }>; + +export const autoblock = async ( + options: AutoblockOptions, + list: BlockerModule[] = blockers() +) => { + if (list.length === 0) { + return null; + } + logger.info('Checking for upgrade blockers...'); const out = await Promise.all( - blockers.map(async (i) => { + list.map(async (i) => { const { blocker } = await i; const result = await blocker.check(options); if (result) { diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts index 5291d9c6bf06..eefc9a946442 100644 --- a/code/lib/cli/src/upgrade.ts +++ b/code/lib/cli/src/upgrade.ts @@ -300,7 +300,7 @@ export const doUpgrade = async ({ function missingStorybookVersionMessage(): string { return dedent` - [Storybook automigrate] ❌ Unable to determine storybook version so the automigrations will be skipped. + [Storybook automigrate] ❌ Unable to determine Storybook version so that the automigrations will be skipped. 🤔 Are you running automigrate from your project directory? Please specify your Storybook config directory with the --config-dir flag. `; } From 1c3aa9b5b4e5a307e3fce4335b3ddefd4429a319 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 7 Feb 2024 13:39:41 +0100 Subject: [PATCH 03/25] fix migrate command that calls the changed runFixes fn --- code/lib/cli/src/migrate.ts | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/code/lib/cli/src/migrate.ts b/code/lib/cli/src/migrate.ts index 5e0093d2480d..5e38507afd61 100644 --- a/code/lib/cli/src/migrate.ts +++ b/code/lib/cli/src/migrate.ts @@ -1,7 +1,11 @@ import { listCodemods, runCodemod } from '@storybook/codemod'; import { runFixes } from './automigrate'; import { bareMdxStoriesGlob } from './automigrate/fixes/bare-mdx-stories-glob'; -import { JsPackageManagerFactory } from '@storybook/core-common'; +import { + JsPackageManagerFactory, + getStorybookInfo, + getCoercedStorybookVersion, +} from '@storybook/core-common'; import { getStorybookVersionSpecifier } from './helpers'; const logger = console; @@ -11,7 +15,33 @@ export async function migrate(migration: any, { glob, dryRun, list, rename, pars listCodemods().forEach((key: any) => logger.log(key)); } else if (migration) { if (migration === 'mdx-to-csf' && !dryRun) { - await runFixes({ fixes: [bareMdxStoriesGlob] }); + const packageManager = JsPackageManagerFactory.getPackageManager(); + + const [packageJson, storybookVersion] = await Promise.all([ + // + packageManager.retrievePackageJson(), + getCoercedStorybookVersion(packageManager), + ]); + const { configDir: inferredConfigDir, mainConfig: mainConfigPath } = + getStorybookInfo(packageJson); + const configDir = inferredConfigDir || '.storybook'; + + // GUARDS + if (!storybookVersion) { + throw new Error('Could not determine Storybook version'); + } + + if (!mainConfigPath) { + throw new Error('Could not determine main config path'); + } + + await runFixes({ + fixes: [bareMdxStoriesGlob], + configDir, + mainConfigPath, + packageManager, + storybookVersion, + }); await addStorybookBlocksPackage(); } await runCodemod(migration, { glob, dryRun, logger, rename, parser }); From da51fee5b860c533e0302593af62c5111596fda3 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 7 Feb 2024 14:31:11 +0100 Subject: [PATCH 04/25] cleanup --- code/lib/cli/src/upgrade.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts index eefc9a946442..a1f84041c7f5 100644 --- a/code/lib/cli/src/upgrade.ts +++ b/code/lib/cli/src/upgrade.ts @@ -229,16 +229,6 @@ export const doUpgrade = async ({ if (!dryRun && !results) { 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(coerceSemver(reactScriptsVersion), '5.0.0')) { - return false; - } - } - // only upgrade packages that are in the monorepo return dependency in versions; }) as Array; From 8af5a5241a9abc54d52716271eee85261ac462f3 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 7 Feb 2024 15:31:06 +0100 Subject: [PATCH 05/25] cleanup --- code/lib/cli/src/upgrade.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts index a1f84041c7f5..a02f4bc7835a 100644 --- a/code/lib/cli/src/upgrade.ts +++ b/code/lib/cli/src/upgrade.ts @@ -20,7 +20,6 @@ import { loadMainConfig, JsPackageManagerFactory, } from '@storybook/core-common'; -import { coerceSemver } from './helpers'; import { automigrate } from './automigrate/index'; import { autoblock } from './autoblock/index'; import { PreCheckFailure } from './automigrate/types'; From d4ca82647ca4261b5442de3c3a00c10c419792a6 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Wed, 7 Feb 2024 15:48:02 +0100 Subject: [PATCH 06/25] remove the types from jsdoc --- code/lib/cli/src/autoblock/types.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/code/lib/cli/src/autoblock/types.ts b/code/lib/cli/src/autoblock/types.ts index 0a59f6ec97f4..a0f66348a3c5 100644 --- a/code/lib/cli/src/autoblock/types.ts +++ b/code/lib/cli/src/autoblock/types.ts @@ -17,22 +17,22 @@ export interface Blocker { /** * Check if the blocker should block. * - * @param {AutoblockOptions} options - The context. - * @returns {Promise} - Return a truthy value to activate the block, return false to proceed. + * @param > The context. + * @returns > Return a truthy value to activate the block, return false to proceed. */ check: (options: AutoblockOptions) => Promise; /** * Format a message to be printed to the log-file. - * @param {AutoblockOptions} options - The context. - * @param {T} data - The data returned from the check method. - * @returns {string} - The string to print to the terminal. + * @param > The context. + * @param > The data returned from the check method. + * @returns > The string to print to the terminal. */ message: (options: AutoblockOptions, data: T) => string; /** * Format a message to be printed to the log-file. - * @param {AutoblockOptions} options - The context. - * @param {T} data - The data returned from the check method. - * @returns {string} - The string to print to the log-file. + * @param > The context. + * @param > The data returned from the check method. + * @returns > The string to print to the log-file. */ log: (options: AutoblockOptions, data: T) => string; } From ab58f407876b7f499faa8fdb94a616d38b8f0b15 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Thu, 8 Feb 2024 00:29:12 +0100 Subject: [PATCH 07/25] the best i can make it.. #bikeshedding --- code/lib/cli/src/autoblock/types.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/code/lib/cli/src/autoblock/types.ts b/code/lib/cli/src/autoblock/types.ts index a0f66348a3c5..62be9625c76e 100644 --- a/code/lib/cli/src/autoblock/types.ts +++ b/code/lib/cli/src/autoblock/types.ts @@ -17,22 +17,22 @@ export interface Blocker { /** * Check if the blocker should block. * - * @param > The context. - * @returns > Return a truthy value to activate the block, return false to proceed. + * @param context + * @returns A truthy value to activate the block, return false to proceed. */ check: (options: AutoblockOptions) => Promise; /** * Format a message to be printed to the log-file. - * @param > The context. - * @param > The data returned from the check method. - * @returns > The string to print to the terminal. + * @param context + * @param data returned from the check method. + * @returns The string to print to the terminal. */ message: (options: AutoblockOptions, data: T) => string; /** * Format a message to be printed to the log-file. - * @param > The context. - * @param > The data returned from the check method. - * @returns > The string to print to the log-file. + * @param context + * @param data returned from the check method. + * @returns The string to print to the log-file. */ log: (options: AutoblockOptions, data: T) => string; } From 668c3a8d741ac45c2dfde8d4d74e6632be68e98f Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Thu, 8 Feb 2024 12:11:29 +0100 Subject: [PATCH 08/25] expand scope of autoblock for react-script to also block vue2 --- .../autoblock/block-dependencies-versions.ts | 67 +++++++++++++++++++ .../autoblock/block-react-script-version.ts | 26 ------- code/lib/cli/src/autoblock/index.ts | 2 + 3 files changed, 69 insertions(+), 26 deletions(-) create mode 100644 code/lib/cli/src/autoblock/block-dependencies-versions.ts delete mode 100644 code/lib/cli/src/autoblock/block-react-script-version.ts diff --git a/code/lib/cli/src/autoblock/block-dependencies-versions.ts b/code/lib/cli/src/autoblock/block-dependencies-versions.ts new file mode 100644 index 000000000000..5e87ed42e820 --- /dev/null +++ b/code/lib/cli/src/autoblock/block-dependencies-versions.ts @@ -0,0 +1,67 @@ +import { createBlocker } from './types'; +import { dedent } from 'ts-dedent'; +import { lt } from 'semver'; + +const minimalVersionsMap = { + 'react-scripts': '5.0.0', + vue: '3.0.0', +}; + +type Result = { + installedVersion: string | undefined; + packageName: keyof typeof minimalVersionsMap; + minimumVersion: string; +}; +const typedKeys = (obj: Record) => Object.keys(obj) as TKey[]; + +export const blocker = createBlocker({ + id: 'dependenciesVersions', + async check({ packageManager }) { + const list = await Promise.all( + typedKeys(minimalVersionsMap).map(async (packageName) => ({ + packageName, + installedVersion: await packageManager.getVersion(packageName), + minimumVersion: minimalVersionsMap[packageName], + })) + ); + + return list.reduce((acc, { installedVersion, minimumVersion, packageName }) => { + if (acc) { + return acc; + } + if (packageName && installedVersion && lt(installedVersion, minimumVersion)) { + return { + installedVersion, + packageName, + minimumVersion, + }; + } + return acc; + }, false); + }, + message(options, data) { + return `Found ${data.packageName} version: ${data.installedVersion}, please upgrade to ${data.minimumVersion} or higher.`; + }, + log(options, data) { + switch (data.packageName) { + case 'react-scripts': + return dedent` + Support react-script < 5.0.0 has been removed. + Please see the migration guide for more information: + https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#create-react-app-dropped-cra4-support + + Upgrade to the latest version of react-scripts. + `; + case 'vue': + return dedent` + Support for Vue 2 has been removed. + Please see the migration guide for more information: + https://v3-migration.vuejs.org/ + + Upgrade to the latest version of Vue. + `; + default: + throw new Error(`Unexpected package name: ${data.packageName}`); + } + }, +}); diff --git a/code/lib/cli/src/autoblock/block-react-script-version.ts b/code/lib/cli/src/autoblock/block-react-script-version.ts deleted file mode 100644 index ad656242456b..000000000000 --- a/code/lib/cli/src/autoblock/block-react-script-version.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createBlocker } from './types'; -import { dedent } from 'ts-dedent'; -import { gte } from 'semver'; - -export const blocker = createBlocker({ - id: 'storiesMdxUsage', - async check({ packageManager }) { - const version = await packageManager.getVersion('react-scripts'); - if (version && gte(version, '5.0.0')) { - return false; - } - return { version }; - }, - message(options, data) { - return `Found react-script version: ${data.version}, please upgrade to latest.`; - }, - log() { - return dedent` - Support react-script < 5.0.0 has been removed. - Please see the migration guide for more information: - https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#create-react-app-dropped-cra4-support - - Upgrade to the latest version of react-scripts. - `; - }, -}); diff --git a/code/lib/cli/src/autoblock/index.ts b/code/lib/cli/src/autoblock/index.ts index e4eb85eb4820..d5c812786747 100644 --- a/code/lib/cli/src/autoblock/index.ts +++ b/code/lib/cli/src/autoblock/index.ts @@ -9,6 +9,8 @@ const excludesFalse = (x: T | false): x is T => x !== false; const blockers: () => BlockerModule[] = () => [ // add/remove blockers here import('./block-storystorev6'), + import('./block-stories-mdx'), + import('./block-dependencies-versions'), ]; type BlockerModule = Promise<{ blocker: Blocker }>; From 2bfea5ff893f8288999f5fb09a440d805f0b085c Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Thu, 8 Feb 2024 12:32:58 +0100 Subject: [PATCH 09/25] add a step in between commander & calling the automigrate function so we get the required context to pass in --- code/lib/cli/src/automigrate/index.ts | 45 +++++++++++++++++++++++++-- code/lib/cli/src/automigrate/types.ts | 13 +++++--- code/lib/cli/src/generate.ts | 4 +-- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/code/lib/cli/src/automigrate/index.ts b/code/lib/cli/src/automigrate/index.ts index f98d15c4f815..19d4ee8922e9 100644 --- a/code/lib/cli/src/automigrate/index.ts +++ b/code/lib/cli/src/automigrate/index.ts @@ -6,9 +6,21 @@ import tempy from 'tempy'; import { join } from 'path'; import invariant from 'tiny-invariant'; -import type { JsPackageManager } from '@storybook/core-common'; - -import type { Fix, FixId, AutofixOptions, FixSummary, PreCheckFailure } from './fixes'; +import { + JsPackageManagerFactory, + type JsPackageManager, + getCoercedStorybookVersion, + getStorybookInfo, +} from '@storybook/core-common'; + +import type { + Fix, + FixId, + AutofixOptions, + FixSummary, + PreCheckFailure, + AutofixOptionsFromCLI, +} from './fixes'; import { FixStatus, allFixes } from './fixes'; import { cleanLog } from './helpers/cleanLog'; import { getMigrationSummary } from './helpers/getMigrationSummary'; @@ -45,6 +57,33 @@ const logAvailableMigrations = () => { logger.info(`\nThe following migrations are available: ${availableFixes}`); }; +export const doAutomigrate = async (options: AutofixOptionsFromCLI) => { + const packageManager = JsPackageManagerFactory.getPackageManager({ + force: options.packageManager, + }); + + const [packageJson, storybookVersion] = await Promise.all([ + packageManager.retrievePackageJson(), + getCoercedStorybookVersion(packageManager), + ]); + + const { configDir: inferredConfigDir, mainConfig: mainConfigPath } = getStorybookInfo( + packageJson, + options.configDir + ); + const configDir = options.configDir || inferredConfigDir || '.storybook'; + + if (!storybookVersion) { + throw new Error('Could not determine Storybook version'); + } + + if (!mainConfigPath) { + throw new Error('Could not determine main config path'); + } + + return automigrate({ ...options, packageManager, storybookVersion, mainConfigPath, configDir }); +}; + export const automigrate = async ({ fixId, fixes: inputFixes, diff --git a/code/lib/cli/src/automigrate/types.ts b/code/lib/cli/src/automigrate/types.ts index e3d13686b96b..97d20c09dc45 100644 --- a/code/lib/cli/src/automigrate/types.ts +++ b/code/lib/cli/src/automigrate/types.ts @@ -1,5 +1,5 @@ import type { StorybookConfigRaw } from '@storybook/types'; -import type { JsPackageManager } from '@storybook/core-common'; +import type { JsPackageManager, PackageManagerName } from '@storybook/core-common'; export interface CheckOptions { packageManager: JsPackageManager; @@ -35,19 +35,22 @@ export enum PreCheckFailure { MAINJS_EVALUATION = 'mainjs_evaluation_error', } -export interface AutofixOptions { +export interface AutofixOptions extends Omit { + packageManager: JsPackageManager; + mainConfigPath: string; + storybookVersion: string; +} +export interface AutofixOptionsFromCLI { fixId?: FixId; list?: boolean; fixes?: Fix[]; yes?: boolean; + packageManager?: PackageManagerName; dryRun?: boolean; - packageManager: JsPackageManager; configDir: string; renderer?: string; skipInstall?: boolean; hideMigrationSummary?: boolean; - mainConfigPath: string; - storybookVersion: string; } export enum FixStatus { diff --git a/code/lib/cli/src/generate.ts b/code/lib/cli/src/generate.ts index 71ea9841fc64..a2280bc8a571 100644 --- a/code/lib/cli/src/generate.ts +++ b/code/lib/cli/src/generate.ts @@ -17,7 +17,7 @@ import { migrate } from './migrate'; import { upgrade, type UpgradeOptions } from './upgrade'; import { sandbox } from './sandbox'; import { link } from './link'; -import { automigrate } from './automigrate'; +import { doAutomigrate } from './automigrate'; import { dev } from './dev'; import { build } from './build'; import { doctor } from './doctor'; @@ -171,7 +171,7 @@ command('automigrate [fixId]') 'The renderer package for the framework Storybook is using.' ) .action(async (fixId, options) => { - await automigrate({ fixId, ...options }).catch((e) => { + await doAutomigrate({ fixId, ...options }).catch((e) => { logger.error(e); process.exit(1); }); From 332b8da2e33cc41aee1289fae21e4bc6b2c7007a Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Thu, 8 Feb 2024 12:48:55 +0100 Subject: [PATCH 10/25] fix incorrect logic on detecting stories.mdx --- code/lib/cli/src/autoblock/block-stories-mdx.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/lib/cli/src/autoblock/block-stories-mdx.ts b/code/lib/cli/src/autoblock/block-stories-mdx.ts index ff726cada631..b6a46cc687bb 100644 --- a/code/lib/cli/src/autoblock/block-stories-mdx.ts +++ b/code/lib/cli/src/autoblock/block-stories-mdx.ts @@ -6,7 +6,7 @@ export const blocker = createBlocker({ id: 'storiesMdxUsage', async check() { const files = await glob('**/*.stories.mdx', { cwd: process.cwd() }); - if (files.length > 0) { + if (files.length === 0) { return false; } return { files }; From c20c3d8b61cfbfb3918782e793144fa24f44043b Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Thu, 8 Feb 2024 12:54:23 +0100 Subject: [PATCH 11/25] make it say 8.0.0 --- code/lib/cli/src/autoblock/block-storystorev6.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/lib/cli/src/autoblock/block-storystorev6.ts b/code/lib/cli/src/autoblock/block-storystorev6.ts index a0cd3d15481e..9b2a0bd487f2 100644 --- a/code/lib/cli/src/autoblock/block-storystorev6.ts +++ b/code/lib/cli/src/autoblock/block-storystorev6.ts @@ -22,7 +22,7 @@ export const blocker = createBlocker({ log() { return dedent` StoryStoreV7 feature must be removed from your Storybook configuration. - This feature was removed in Storybook 7.0.0. + This feature was removed in Storybook 8.0.0. Please see the migration guide for more information: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#story-store-v7 From 9ca5d50ca8ed51c29fda24e68956306f65e29936 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Thu, 8 Feb 2024 12:57:13 +0100 Subject: [PATCH 12/25] check for the INSTALLED version, not the latest --- code/lib/cli/src/autoblock/block-dependencies-versions.ts | 2 +- .../lib/core-common/src/js-package-manager/JsPackageManager.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/code/lib/cli/src/autoblock/block-dependencies-versions.ts b/code/lib/cli/src/autoblock/block-dependencies-versions.ts index 5e87ed42e820..78c93ca577bb 100644 --- a/code/lib/cli/src/autoblock/block-dependencies-versions.ts +++ b/code/lib/cli/src/autoblock/block-dependencies-versions.ts @@ -20,7 +20,7 @@ export const blocker = createBlocker({ const list = await Promise.all( typedKeys(minimalVersionsMap).map(async (packageName) => ({ packageName, - installedVersion: await packageManager.getVersion(packageName), + installedVersion: await packageManager.getPackageVersion(packageName), minimumVersion: minimalVersionsMap[packageName], })) ); diff --git a/code/lib/core-common/src/js-package-manager/JsPackageManager.ts b/code/lib/core-common/src/js-package-manager/JsPackageManager.ts index 9c386ae0861f..8523d7224eda 100644 --- a/code/lib/core-common/src/js-package-manager/JsPackageManager.ts +++ b/code/lib/core-common/src/js-package-manager/JsPackageManager.ts @@ -54,6 +54,9 @@ export abstract class JsPackageManager { basePath?: string ): Promise; + /** + * Get the INSTALLED version of a package from the package.json file + */ async getPackageVersion(packageName: string, basePath = this.cwd): Promise { const packageJSON = await this.getPackageJSON(packageName, basePath); return packageJSON ? packageJSON.version ?? null : null; From b12052cca444c6918068231b08f3f20f743b92e2 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Thu, 8 Feb 2024 14:01:47 +0100 Subject: [PATCH 13/25] add a automigration for a old vite version and another one for ensure the user has a vite config file with the right plugins in there --- code/lib/cli/src/automigrate/fixes/index.ts | 4 + .../src/automigrate/fixes/vite-config-file.ts | 95 +++++++++++++++++++ code/lib/cli/src/automigrate/fixes/vite4.ts | 43 +++++++++ 3 files changed, 142 insertions(+) create mode 100644 code/lib/cli/src/automigrate/fixes/vite-config-file.ts create mode 100644 code/lib/cli/src/automigrate/fixes/vite4.ts diff --git a/code/lib/cli/src/automigrate/fixes/index.ts b/code/lib/cli/src/automigrate/fixes/index.ts index 27f8a80b3140..354b9410ac97 100644 --- a/code/lib/cli/src/automigrate/fixes/index.ts +++ b/code/lib/cli/src/automigrate/fixes/index.ts @@ -2,10 +2,12 @@ import type { Fix } from '../types'; import { cra5 } from './cra5'; import { webpack5 } from './webpack5'; +import { vite4 } from './vite4'; import { vue3 } from './vue3'; import { mdxgfm } from './mdx-gfm'; import { eslintPlugin } from './eslint-plugin'; import { builderVite } from './builder-vite'; +import { viteConfigFile } from './vite-config-file'; import { sbScripts } from './sb-scripts'; import { sbBinary } from './sb-binary'; import { newFrameworks } from './new-frameworks'; @@ -29,6 +31,8 @@ export const allFixes: Fix[] = [ cra5, webpack5, vue3, + vite4, + viteConfigFile, eslintPlugin, builderVite, sbBinary, diff --git a/code/lib/cli/src/automigrate/fixes/vite-config-file.ts b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts new file mode 100644 index 000000000000..e6983c3677ed --- /dev/null +++ b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts @@ -0,0 +1,95 @@ +import { dedent } from 'ts-dedent'; +import type { Fix } from '../types'; +import findUp from 'find-up'; + +interface Webpack5RunOptions { + plugins: string[]; + existed: boolean; +} + +export const viteConfigFile = { + id: 'viteConfigFile', + + async check({ mainConfig, packageManager }) { + const viteConfigPath = await findUp('vite.config.js'); + + const rendererToVitePluginMap: Record = { + preact: '@preact/preset-vite', + qwik: 'vite-plugin-qwik', + react: '@vitejs/plugin-react', + solid: 'vite-plugin-solid', + svelte: '@sveltejs/vite-plugin-svelte', + vue: '@vitejs/plugin-vue', + }; + + // TODO: cleanup this logic + const frameworkName = + typeof mainConfig.framework === 'string' ? mainConfig.framework : mainConfig.framework?.name; + const isUsingViteBuilder = + mainConfig.core?.builder === 'vite' || + frameworkName?.includes('vite') || + frameworkName === 'qwik' || + frameworkName === 'sveltekit' || + frameworkName === 'solid'; + + const rendererName = (mainConfig.core?.renderer || frameworkName?.split('-')[0]) as string; + + if (!viteConfigPath && isUsingViteBuilder) { + const plugins = []; + + if (rendererToVitePluginMap[rendererName]) { + plugins.push(rendererToVitePluginMap[rendererName]); + } + + return { + plugins, + existed: !!viteConfigPath, + }; + } + + const plugin = rendererToVitePluginMap[rendererName]; + const pluginVersion = await packageManager.getPackageVersion(plugin); + + if (viteConfigPath && isUsingViteBuilder && !pluginVersion) { + const plugins = []; + + if (plugin) { + plugins.push(plugin); + } + + return { + plugins, + existed: !!viteConfigPath, + }; + } + + return null; + }, + + prompt({ existed, plugins }) { + if (existed) { + return dedent` + Storybook 8.0.0 no longers ships with a vite config build-in. + We've detected you do have a vite config, but you may be missing the following plugins in it. + + ${plugins.map((plugin) => ` - ${plugin}`).join('\n')} + + If you do already have these plugins, you can ignore this message. + + You can find more information on how to do this here: + https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#framework-specific-vite-plugins-have-to-be-explicitly-added + + This change was necessary to support newer versions of vite. + `; + } + return dedent` + Storybook 8.0.0 no longers ships with a vite config build-in. + Please add a vite.config.js file to your project root. + + You can find more information on how to do this here: + https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#framework-specific-vite-plugins-have-to-be-explicitly-added + + This change was necessary to support newer versions of vite. + `; + }, +} satisfies Fix; diff --git a/code/lib/cli/src/automigrate/fixes/vite4.ts b/code/lib/cli/src/automigrate/fixes/vite4.ts new file mode 100644 index 000000000000..d04c4abd10d7 --- /dev/null +++ b/code/lib/cli/src/automigrate/fixes/vite4.ts @@ -0,0 +1,43 @@ +import chalk from 'chalk'; +import { dedent } from 'ts-dedent'; +import semver from 'semver'; +import type { Fix } from '../types'; + +const logger = console; + +interface Webpack5RunOptions { + viteVersion: string | null; +} + +export const vite4 = { + id: 'vite4', + + async check({ packageManager }) { + const viteVersion = await packageManager.getPackageVersion('vite'); + + if (!viteVersion || semver.gt(viteVersion, '4.0.0')) { + return null; + } + + return { viteVersion }; + }, + + prompt({ viteVersion: viteVersion }) { + const viteFormatted = chalk.cyan(`${viteVersion}`); + + return dedent` + We've detected your version of Vite is outdated (${viteFormatted}). + + Storybook 8.0.0 will require Vite 4.0.0 or later. + Do you want us to upgrade Vite for you? + `; + }, + + async run({ packageManager, dryRun }) { + const deps = [`vite`]; + logger.info(`✅ Adding dependencies: ${deps}`); + if (!dryRun) { + await packageManager.addDependencies({ installAsDevDependencies: true }, deps); + } + }, +} satisfies Fix; From 30bed6220a79b89a2d6db6f30bd90ecc25ff264a Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Thu, 8 Feb 2024 14:09:15 +0100 Subject: [PATCH 14/25] add migration note, and early return --- MIGRATION.md | 64 ++++++++++++++++++- .../src/automigrate/fixes/vite-config-file.ts | 7 ++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index 15b17681953b..a8fb0e1d43db 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -7,6 +7,12 @@ - [Removal of `storiesOf`-API](#removal-of-storiesof-api) - [Removed deprecated shim packages](#removed-deprecated-shim-packages) - [Framework-specific Vite plugins have to be explicitly added](#framework-specific-vite-plugins-have-to-be-explicitly-added) + - [For React:](#for-react) + - [For Vue:](#for-vue) + - [For Svelte (without Sveltekit):](#for-svelte-without-sveltekit) + - [For Preact:](#for-preact) + - [For Solid:](#for-solid) + - [For Qwik:](#for-qwik) - [Implicit actions can not be used during rendering (for example in the play function)](#implicit-actions-can-not-be-used-during-rendering-for-example-in-the-play-function) - [MDX related changes](#mdx-related-changes) - [MDX is upgraded to v3](#mdx-is-upgraded-to-v3) @@ -450,16 +456,72 @@ This section explains the rationale, and the required changed you might have to In Storybook 7, we would automatically add frameworks-specific Vite plugins, e.g. `@vitejs/plugin-react` if not installed. In Storybook 8 those plugins have to be added explicitly in the user's `vite.config.ts`: +#### For React: + ```ts import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; -// https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], }); ``` +#### For Vue: + +```ts +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; + +export default defineConfig({ + plugins: [vue()], +}); +``` + +#### For Svelte (without Sveltekit): + +```ts +import { defineConfig } from "vite"; +import svelte from "@sveltejs/vite-plugin-svelte"; + +export default defineConfig({ + plugins: [svelte()], +}); +``` + +#### For Preact: + +```ts +import { defineConfig } from "vite"; +import preact from "@preact/preset-vite"; + +export default defineConfig({ + plugins: [preact()], +}); +``` + +#### For Solid: + +```ts +import { defineConfig } from "vite"; +import solid from "vite-plugin-solid"; + +export default defineConfig({ + plugins: [solid()], +}); +``` + +#### For Qwik: + +```ts +import { defineConfig } from "vite"; +import qwik from "vite-plugin-qwik"; + +export default defineConfig({ + plugins: [qwik()], +}); +``` + ### Implicit actions can not be used during rendering (for example in the play function) In Storybook 7, we inferred if the component accepts any action props, diff --git a/code/lib/cli/src/automigrate/fixes/vite-config-file.ts b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts index e6983c3677ed..df6649f023cd 100644 --- a/code/lib/cli/src/automigrate/fixes/vite-config-file.ts +++ b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts @@ -19,6 +19,7 @@ export const viteConfigFile = { react: '@vitejs/plugin-react', solid: 'vite-plugin-solid', svelte: '@sveltejs/vite-plugin-svelte', + sveltekit: '@sveltejs/kit/vite', // might be pointless? vue: '@vitejs/plugin-vue', }; @@ -32,6 +33,7 @@ export const viteConfigFile = { frameworkName === 'sveltekit' || frameworkName === 'solid'; + // TODO: cleanup this logic const rendererName = (mainConfig.core?.renderer || frameworkName?.split('-')[0]) as string; if (!viteConfigPath && isUsingViteBuilder) { @@ -48,6 +50,11 @@ export const viteConfigFile = { } const plugin = rendererToVitePluginMap[rendererName]; + + if (!plugin) { + return null; + } + const pluginVersion = await packageManager.getPackageVersion(plugin); if (viteConfigPath && isUsingViteBuilder && !pluginVersion) { From 3b2098276fc27d7684591f674bf6cb6b29ce11ca Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Fri, 9 Feb 2024 12:22:48 +0100 Subject: [PATCH 15/25] use getFrameworkPackageName --- code/lib/cli/src/automigrate/fixes/vite-config-file.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/lib/cli/src/automigrate/fixes/vite-config-file.ts b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts index df6649f023cd..8f1c471043cf 100644 --- a/code/lib/cli/src/automigrate/fixes/vite-config-file.ts +++ b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts @@ -1,6 +1,7 @@ import { dedent } from 'ts-dedent'; import type { Fix } from '../types'; import findUp from 'find-up'; +import { getFrameworkPackageName } from '../helpers/mainConfigFile'; interface Webpack5RunOptions { plugins: string[]; @@ -24,8 +25,7 @@ export const viteConfigFile = { }; // TODO: cleanup this logic - const frameworkName = - typeof mainConfig.framework === 'string' ? mainConfig.framework : mainConfig.framework?.name; + const frameworkName = getFrameworkPackageName(mainConfig); const isUsingViteBuilder = mainConfig.core?.builder === 'vite' || frameworkName?.includes('vite') || From b8e1a75be4e3585bdd99807d22233dfd773e1605 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Fri, 9 Feb 2024 17:16:26 +0100 Subject: [PATCH 16/25] performed some manual testing and found some issues --- .../src/automigrate/fixes/vite-config-file.ts | 23 +++++++++++++------ code/lib/cli/src/helpers.ts | 2 +- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/code/lib/cli/src/automigrate/fixes/vite-config-file.ts b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts index 8f1c471043cf..5e7bac60a3a6 100644 --- a/code/lib/cli/src/automigrate/fixes/vite-config-file.ts +++ b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts @@ -2,6 +2,7 @@ import { dedent } from 'ts-dedent'; import type { Fix } from '../types'; import findUp from 'find-up'; import { getFrameworkPackageName } from '../helpers/mainConfigFile'; +import { frameworkToRenderer } from '../../helpers'; interface Webpack5RunOptions { plugins: string[]; @@ -12,7 +13,12 @@ export const viteConfigFile = { id: 'viteConfigFile', async check({ mainConfig, packageManager }) { - const viteConfigPath = await findUp('vite.config.js'); + const viteConfigPath = await findUp([ + 'vite.config.js', + 'vite.config.mjs', + 'vite.config.cjs', + 'vite.config.ts', + ]); const rendererToVitePluginMap: Record = { preact: '@preact/preset-vite', @@ -24,17 +30,20 @@ export const viteConfigFile = { vue: '@vitejs/plugin-vue', }; - // TODO: cleanup this logic const frameworkName = getFrameworkPackageName(mainConfig); const isUsingViteBuilder = mainConfig.core?.builder === 'vite' || frameworkName?.includes('vite') || frameworkName === 'qwik' || - frameworkName === 'sveltekit' || - frameworkName === 'solid'; + frameworkName === 'solid' || + frameworkName === 'sveltekit'; - // TODO: cleanup this logic - const rendererName = (mainConfig.core?.renderer || frameworkName?.split('-')[0]) as string; + if (!frameworkName) { + return null; + } + + const key = frameworkName?.replace('@storybook/', '') as keyof typeof frameworkToRenderer; + const rendererName = frameworkToRenderer[key]; if (!viteConfigPath && isUsingViteBuilder) { const plugins = []; @@ -66,7 +75,7 @@ export const viteConfigFile = { return { plugins, - existed: !!viteConfigPath, + existed: !viteConfigPath, }; } diff --git a/code/lib/cli/src/helpers.ts b/code/lib/cli/src/helpers.ts index 95c22ee624f1..bbf81816a061 100644 --- a/code/lib/cli/src/helpers.ts +++ b/code/lib/cli/src/helpers.ts @@ -130,7 +130,7 @@ type CopyTemplateFilesOptions = { destination?: string; }; -const frameworkToRenderer: Record< +export const frameworkToRenderer: Record< SupportedFrameworks | SupportedRenderers, SupportedRenderers | 'vue' > = { From fb144f270473fe39b5b3948987ecb081837666be Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Mon, 12 Feb 2024 11:30:32 +0100 Subject: [PATCH 17/25] fix review comment --- .../src/automigrate/fixes/vite-config-file.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/code/lib/cli/src/automigrate/fixes/vite-config-file.ts b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts index 5e7bac60a3a6..205b45bffea0 100644 --- a/code/lib/cli/src/automigrate/fixes/vite-config-file.ts +++ b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts @@ -3,6 +3,7 @@ import type { Fix } from '../types'; import findUp from 'find-up'; import { getFrameworkPackageName } from '../helpers/mainConfigFile'; import { frameworkToRenderer } from '../../helpers'; +import { frameworkPackages } from '@storybook/core-common'; interface Webpack5RunOptions { plugins: string[]; @@ -30,20 +31,19 @@ export const viteConfigFile = { vue: '@vitejs/plugin-vue', }; - const frameworkName = getFrameworkPackageName(mainConfig); - const isUsingViteBuilder = - mainConfig.core?.builder === 'vite' || - frameworkName?.includes('vite') || - frameworkName === 'qwik' || - frameworkName === 'solid' || - frameworkName === 'sveltekit'; - - if (!frameworkName) { + const frameworkPackageName = getFrameworkPackageName(mainConfig); + if (!frameworkPackageName) { return null; } + const frameworkName = frameworkPackages[frameworkPackageName]; + const isUsingViteBuilder = + mainConfig.core?.builder === 'vite' || + frameworkPackageName?.includes('vite') || + frameworkPackageName === 'qwik' || + frameworkPackageName === 'solid' || + frameworkPackageName === 'sveltekit'; - const key = frameworkName?.replace('@storybook/', '') as keyof typeof frameworkToRenderer; - const rendererName = frameworkToRenderer[key]; + const rendererName = frameworkToRenderer[frameworkName as keyof typeof frameworkToRenderer]; if (!viteConfigPath && isUsingViteBuilder) { const plugins = []; From 68e6cb0d54a47e92c5b8878a201dfd64e6618d28 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Mon, 12 Feb 2024 13:02:10 +0100 Subject: [PATCH 18/25] add more autoblockers for dependencies & add a --force flag to skip autoblockers --- .../autoblock/block-dependencies-versions.ts | 31 +++++++++++++++++-- code/lib/cli/src/generate.ts | 1 + code/lib/cli/src/upgrade.ts | 8 ++++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/code/lib/cli/src/autoblock/block-dependencies-versions.ts b/code/lib/cli/src/autoblock/block-dependencies-versions.ts index 78c93ca577bb..e9f2c9b2c780 100644 --- a/code/lib/cli/src/autoblock/block-dependencies-versions.ts +++ b/code/lib/cli/src/autoblock/block-dependencies-versions.ts @@ -3,8 +3,14 @@ import { dedent } from 'ts-dedent'; import { lt } from 'semver'; const minimalVersionsMap = { + '@angular/core': '15.0.0', 'react-scripts': '5.0.0', + nextjs: '13.5.0', + preact: '10.0.0', + svelte: '4.0.0', + vite: '4.0.0', vue: '3.0.0', + webpack: '5.0.0', }; type Result = { @@ -56,12 +62,33 @@ export const blocker = createBlocker({ return dedent` Support for Vue 2 has been removed. Please see the migration guide for more information: + https://angular.io/guide/update-to-version-15 + + Please upgrade to the latest version of Vue. + `; + case '@angular/core': + return dedent` + Support for Angular < 15 has been removed. + Please see the migration guide for more information: https://v3-migration.vuejs.org/ - Upgrade to the latest version of Vue. + Please upgrade to the latest version of Angular. + `; + case 'nextjs': + return dedent` + Support for NextJS < 13.5 has been removed. + Please see the migration guide for more information: + https://nextjs.org/docs/pages/building-your-application/upgrading/version-13 + + Please upgrade to the latest version of NextJS. `; default: - throw new Error(`Unexpected package name: ${data.packageName}`); + return dedent` + Support for ${data.packageName} version < ${data.minimumVersion} has been removed. + Storybook 8 needs minimum version of ${data.minimumVersion}, but you had version ${data.installedVersion}. + + Please update this dependency. + `; } }, }); diff --git a/code/lib/cli/src/generate.ts b/code/lib/cli/src/generate.ts index a2280bc8a571..250cd200d206 100644 --- a/code/lib/cli/src/generate.ts +++ b/code/lib/cli/src/generate.ts @@ -80,6 +80,7 @@ command('upgrade') 'Force package manager for installing dependencies' ) .option('-y --yes', 'Skip prompting the user') + .option('-f --force', 'force the upgrade, skipping autoblockers') .option('-n --dry-run', 'Only check for upgrades, do not install') .option('-s --skip-check', 'Skip postinstall version and automigration checks') .option('-c, --config-dir ', 'Directory where to load Storybook configurations from') diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts index a02f4bc7835a..53bf96534b31 100644 --- a/code/lib/cli/src/upgrade.ts +++ b/code/lib/cli/src/upgrade.ts @@ -114,6 +114,7 @@ export interface UpgradeOptions { packageManager?: PackageManagerName; dryRun: boolean; yes: boolean; + force: boolean; disableTelemetry: boolean; configDir?: string; } @@ -211,7 +212,12 @@ export const doUpgrade = async ({ } // BLOCKERS - if (!results && typeof mainConfig !== 'boolean' && typeof mainConfigPath !== 'undefined') { + if ( + !results && + typeof mainConfig !== 'boolean' && + typeof mainConfigPath !== 'undefined' && + options.force + ) { const blockResult = await autoblock({ packageManager, configDir, From 369704bb40be8f829da367804211e912d2432031 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Mon, 12 Feb 2024 13:14:51 +0100 Subject: [PATCH 19/25] move nodeversion check to autoblock --- .../autoblock/block-dependencies-versions.ts | 2 - .../cli/src/autoblock/block-node-version.ts | 25 ++++++++++ .../fixes/nodejs-requirement.test.ts | 49 ------------------- .../automigrate/fixes/nodejs-requirement.ts | 44 ----------------- 4 files changed, 25 insertions(+), 95 deletions(-) create mode 100644 code/lib/cli/src/autoblock/block-node-version.ts delete mode 100644 code/lib/cli/src/automigrate/fixes/nodejs-requirement.test.ts delete mode 100644 code/lib/cli/src/automigrate/fixes/nodejs-requirement.ts diff --git a/code/lib/cli/src/autoblock/block-dependencies-versions.ts b/code/lib/cli/src/autoblock/block-dependencies-versions.ts index e9f2c9b2c780..4cce2b2e70b5 100644 --- a/code/lib/cli/src/autoblock/block-dependencies-versions.ts +++ b/code/lib/cli/src/autoblock/block-dependencies-versions.ts @@ -8,9 +8,7 @@ const minimalVersionsMap = { nextjs: '13.5.0', preact: '10.0.0', svelte: '4.0.0', - vite: '4.0.0', vue: '3.0.0', - webpack: '5.0.0', }; type Result = { diff --git a/code/lib/cli/src/autoblock/block-node-version.ts b/code/lib/cli/src/autoblock/block-node-version.ts new file mode 100644 index 000000000000..978360c6bc55 --- /dev/null +++ b/code/lib/cli/src/autoblock/block-node-version.ts @@ -0,0 +1,25 @@ +import { createBlocker } from './types'; +import { dedent } from 'ts-dedent'; +import { lt } from 'semver'; + +export const blocker = createBlocker({ + id: 'minimumNode16', + async check() { + const nodeVersion = process.version; + if (lt(nodeVersion, '16.0.0')) { + return { nodeVersion }; + } + return false; + }, + message(options, data) { + return `Please use NodeJS v16 or higher.`; + }, + log(options, data) { + return dedent` + We've detected you're using NodeJS v${data.nodeVersion}. + Storybook needs at least NodeJS 16. + + https://nodejs.org/en/download + `; + }, +}); diff --git a/code/lib/cli/src/automigrate/fixes/nodejs-requirement.test.ts b/code/lib/cli/src/automigrate/fixes/nodejs-requirement.test.ts deleted file mode 100644 index b3c1f8b311d6..000000000000 --- a/code/lib/cli/src/automigrate/fixes/nodejs-requirement.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, afterAll, it, expect, vi } from 'vitest'; - -import { nodeJsRequirement } from './nodejs-requirement'; - -vi.mock('fs-extra', async () => import('../../../../../__mocks__/fs-extra')); - -const check = async ({ storybookVersion = '7.0.0' }) => { - return nodeJsRequirement.check({ - storybookVersion, - packageManager: {} as any, - mainConfig: {} as any, - }); -}; - -const originalNodeVersion = process.version; -const mockNodeVersion = (version: string) => { - Object.defineProperties(process, { - version: { - value: version, - }, - }); -}; - -describe('nodejs-requirement fix', () => { - afterAll(() => { - mockNodeVersion(originalNodeVersion); - vi.restoreAllMocks(); - }); - - it('skips when sb <= 7.0.0', async () => { - mockNodeVersion('14.0.0'); - await expect(check({ storybookVersion: '6.3.2' })).resolves.toBeNull(); - }); - - it('skips when node >= 16.0.0', async () => { - mockNodeVersion('16.0.0'); - await expect(check({})).resolves.toBeNull(); - }); - - it('skips when node >= 18.0.0', async () => { - mockNodeVersion('18.0.0'); - await expect(check({})).resolves.toBeNull(); - }); - - it('prompts when node <= 16.0.0', async () => { - mockNodeVersion('14.0.0'); - await expect(check({})).resolves.toEqual({ nodeVersion: '14.0.0' }); - }); -}); diff --git a/code/lib/cli/src/automigrate/fixes/nodejs-requirement.ts b/code/lib/cli/src/automigrate/fixes/nodejs-requirement.ts deleted file mode 100644 index cf82bceb9bac..000000000000 --- a/code/lib/cli/src/automigrate/fixes/nodejs-requirement.ts +++ /dev/null @@ -1,44 +0,0 @@ -import chalk from 'chalk'; -import dedent from 'ts-dedent'; -import semver from 'semver'; -import type { Fix } from '../types'; - -interface NodeJsRequirementOptions { - nodeVersion: string; -} - -export const nodeJsRequirement: Fix = { - id: 'nodejs-requirement', - promptOnly: true, - - async check({ storybookVersion }) { - if (!semver.gte(storybookVersion, '7.0.0')) { - return null; - } - - const nodeVersion = process.version; - if (semver.lt(nodeVersion, '16.0.0')) { - return { nodeVersion }; - } - - return null; - }, - prompt({ nodeVersion }) { - return dedent` - ${chalk.bold( - chalk.red('Attention') - )}: We could not automatically make this change. You'll need to do it manually. - - We've detected that you're using Node ${chalk.bold( - nodeVersion - )} but Storybook 7 only supports Node ${chalk.bold( - 'v16.0.0' - )} and higher. You will either need to upgrade your Node version or keep using an older version of Storybook. - - Please see the migration guide for more information: - ${chalk.yellow( - 'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#dropped-support-for-node-15-and-below' - )} - `; - }, -}; From 0003c4043901bb7d612ef45b410d290d5b954144 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Mon, 12 Feb 2024 13:19:21 +0100 Subject: [PATCH 20/25] remove from list --- code/lib/cli/src/automigrate/fixes/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/code/lib/cli/src/automigrate/fixes/index.ts b/code/lib/cli/src/automigrate/fixes/index.ts index 354b9410ac97..7715e4efe0ce 100644 --- a/code/lib/cli/src/automigrate/fixes/index.ts +++ b/code/lib/cli/src/automigrate/fixes/index.ts @@ -14,7 +14,6 @@ import { newFrameworks } from './new-frameworks'; import { removedGlobalClientAPIs } from './remove-global-client-apis'; import { mdx1to2 } from './mdx-1-to-2'; import { autodocsTrue } from './autodocs-true'; -import { nodeJsRequirement } from './nodejs-requirement'; import { angularBuilders } from './angular-builders'; import { incompatibleAddons } from './incompatible-addons'; import { angularBuildersMultiproject } from './angular-builders-multiproject'; @@ -26,7 +25,6 @@ import { storyshotsMigration } from './storyshots-migration'; export * from '../types'; export const allFixes: Fix[] = [ - nodeJsRequirement, newFrameworks, cra5, webpack5, From 6344a4aebf0960cb65c2c1ac459fc4d70c0d157a Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Mon, 12 Feb 2024 14:22:43 +0100 Subject: [PATCH 21/25] Apply suggestions from code review Co-authored-by: Valentin Palkovic --- .../cli/src/autoblock/block-dependencies-versions.ts | 8 ++++---- code/lib/cli/src/autoblock/block-node-version.ts | 6 +++--- code/lib/cli/src/automigrate/fixes/vite-config-file.ts | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/code/lib/cli/src/autoblock/block-dependencies-versions.ts b/code/lib/cli/src/autoblock/block-dependencies-versions.ts index 4cce2b2e70b5..e4d7e595206f 100644 --- a/code/lib/cli/src/autoblock/block-dependencies-versions.ts +++ b/code/lib/cli/src/autoblock/block-dependencies-versions.ts @@ -60,7 +60,7 @@ export const blocker = createBlocker({ return dedent` Support for Vue 2 has been removed. Please see the migration guide for more information: - https://angular.io/guide/update-to-version-15 + https://v3-migration.vuejs.org/ Please upgrade to the latest version of Vue. `; @@ -68,17 +68,17 @@ export const blocker = createBlocker({ return dedent` Support for Angular < 15 has been removed. Please see the migration guide for more information: - https://v3-migration.vuejs.org/ + https://angular.io/guide/update-to-version-15 Please upgrade to the latest version of Angular. `; case 'nextjs': return dedent` - Support for NextJS < 13.5 has been removed. + Support for Next.js < 13.5 has been removed. Please see the migration guide for more information: https://nextjs.org/docs/pages/building-your-application/upgrading/version-13 - Please upgrade to the latest version of NextJS. + Please upgrade to the latest version of Next.js. `; default: return dedent` diff --git a/code/lib/cli/src/autoblock/block-node-version.ts b/code/lib/cli/src/autoblock/block-node-version.ts index 978360c6bc55..aef0cc8b6b2b 100644 --- a/code/lib/cli/src/autoblock/block-node-version.ts +++ b/code/lib/cli/src/autoblock/block-node-version.ts @@ -12,12 +12,12 @@ export const blocker = createBlocker({ return false; }, message(options, data) { - return `Please use NodeJS v16 or higher.`; + return `Please use Node.js v16 or higher.`; }, log(options, data) { return dedent` - We've detected you're using NodeJS v${data.nodeVersion}. - Storybook needs at least NodeJS 16. + We've detected you're using Node.js v${data.nodeVersion}. + Storybook needs at least Node.js 16. https://nodejs.org/en/download `; diff --git a/code/lib/cli/src/automigrate/fixes/vite-config-file.ts b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts index 205b45bffea0..f8047a839af9 100644 --- a/code/lib/cli/src/automigrate/fixes/vite-config-file.ts +++ b/code/lib/cli/src/automigrate/fixes/vite-config-file.ts @@ -85,8 +85,8 @@ export const viteConfigFile = { prompt({ existed, plugins }) { if (existed) { return dedent` - Storybook 8.0.0 no longers ships with a vite config build-in. - We've detected you do have a vite config, but you may be missing the following plugins in it. + Storybook 8.0.0 no longer ships with a Vite config build-in. + We've detected you do have a Vite config, but you may be missing the following plugins in it. ${plugins.map((plugin) => ` - ${plugin}`).join('\n')} @@ -95,17 +95,17 @@ export const viteConfigFile = { You can find more information on how to do this here: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#framework-specific-vite-plugins-have-to-be-explicitly-added - This change was necessary to support newer versions of vite. + This change was necessary to support newer versions of Vite. `; } return dedent` - Storybook 8.0.0 no longers ships with a vite config build-in. + Storybook 8.0.0 no longer ships with a Vite config build-in. Please add a vite.config.js file to your project root. You can find more information on how to do this here: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#framework-specific-vite-plugins-have-to-be-explicitly-added - This change was necessary to support newer versions of vite. + This change was necessary to support newer versions of Vite. `; }, } satisfies Fix; From a4d408713ac0166973e973b22718b6e3e9b69048 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Mon, 12 Feb 2024 15:37:07 +0100 Subject: [PATCH 22/25] apply https://github.com/storybookjs/storybook/pull/25934#discussion_r1486145990 --- code/lib/cli/src/autoblock/block-node-version.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/lib/cli/src/autoblock/block-node-version.ts b/code/lib/cli/src/autoblock/block-node-version.ts index aef0cc8b6b2b..fe35792a4a74 100644 --- a/code/lib/cli/src/autoblock/block-node-version.ts +++ b/code/lib/cli/src/autoblock/block-node-version.ts @@ -5,7 +5,7 @@ import { lt } from 'semver'; export const blocker = createBlocker({ id: 'minimumNode16', async check() { - const nodeVersion = process.version; + const nodeVersion = process.versions.node; if (lt(nodeVersion, '16.0.0')) { return { nodeVersion }; } From 243a60d4b3eec3be54a138d53efc6a58814a1ef1 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 13 Feb 2024 11:12:19 +0100 Subject: [PATCH 23/25] add back the removed renderer mapping --- code/lib/core-common/src/utils/get-storybook-info.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/code/lib/core-common/src/utils/get-storybook-info.ts b/code/lib/core-common/src/utils/get-storybook-info.ts index dd462a6b0370..acf5aae77b91 100644 --- a/code/lib/core-common/src/utils/get-storybook-info.ts +++ b/code/lib/core-common/src/utils/get-storybook-info.ts @@ -14,9 +14,15 @@ export const rendererPackages: Record = { '@storybook/svelte': 'svelte', '@storybook/preact': 'preact', '@storybook/server': 'server', + // community (outside of monorepo) 'storybook-framework-qwik': 'qwik', 'storybook-solidjs': 'solid', + + /** + * @deprecated This is deprecated. + */ + '@storybook/vue': 'vue', }; export const frameworkPackages: Record = { From d79043f58c65add79ca3142e3f90b3261ce4ea9a Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 13 Feb 2024 11:21:54 +0100 Subject: [PATCH 24/25] Update code/lib/cli/src/autoblock/block-stories-mdx.ts Co-authored-by: Valentin Palkovic --- code/lib/cli/src/autoblock/block-stories-mdx.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/code/lib/cli/src/autoblock/block-stories-mdx.ts b/code/lib/cli/src/autoblock/block-stories-mdx.ts index b6a46cc687bb..b868d913ecd0 100644 --- a/code/lib/cli/src/autoblock/block-stories-mdx.ts +++ b/code/lib/cli/src/autoblock/block-stories-mdx.ts @@ -12,7 +12,9 @@ export const blocker = createBlocker({ return { files }; }, message(options, data) { - return `Found ${data.files.length} stories.mdx files, these must be migrated.`; + return `Found ${data.files.length} stories.mdx ${ + data.files.length === 1 ? 'file' : 'files' + }, these must be migrated.`; }, log() { return dedent` From 2aec740ace485104552f3c052d6e2710282bd968 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Tue, 13 Feb 2024 11:22:18 +0100 Subject: [PATCH 25/25] fixes --- code/lib/cli/src/autoblock/block-dependencies-versions.ts | 4 ++-- code/lib/cli/src/autoblock/block-node-version.ts | 6 +++--- code/lib/cli/src/autoblock/block-storystorev6.ts | 2 +- code/lib/cli/src/autoblock/index.ts | 1 + code/lib/cli/src/upgrade.ts | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/code/lib/cli/src/autoblock/block-dependencies-versions.ts b/code/lib/cli/src/autoblock/block-dependencies-versions.ts index e4d7e595206f..284562aa9f6d 100644 --- a/code/lib/cli/src/autoblock/block-dependencies-versions.ts +++ b/code/lib/cli/src/autoblock/block-dependencies-versions.ts @@ -5,7 +5,7 @@ import { lt } from 'semver'; const minimalVersionsMap = { '@angular/core': '15.0.0', 'react-scripts': '5.0.0', - nextjs: '13.5.0', + next: '13.5.0', preact: '10.0.0', svelte: '4.0.0', vue: '3.0.0', @@ -72,7 +72,7 @@ export const blocker = createBlocker({ Please upgrade to the latest version of Angular. `; - case 'nextjs': + case 'next': return dedent` Support for Next.js < 13.5 has been removed. Please see the migration guide for more information: diff --git a/code/lib/cli/src/autoblock/block-node-version.ts b/code/lib/cli/src/autoblock/block-node-version.ts index fe35792a4a74..220b29823e4e 100644 --- a/code/lib/cli/src/autoblock/block-node-version.ts +++ b/code/lib/cli/src/autoblock/block-node-version.ts @@ -6,18 +6,18 @@ export const blocker = createBlocker({ id: 'minimumNode16', async check() { const nodeVersion = process.versions.node; - if (lt(nodeVersion, '16.0.0')) { + if (nodeVersion && lt(nodeVersion, '18.0.0')) { return { nodeVersion }; } return false; }, message(options, data) { - return `Please use Node.js v16 or higher.`; + return `Please use Node.js v18 or higher.`; }, log(options, data) { return dedent` We've detected you're using Node.js v${data.nodeVersion}. - Storybook needs at least Node.js 16. + Storybook needs Node.js 18 or higher. https://nodejs.org/en/download `; diff --git a/code/lib/cli/src/autoblock/block-storystorev6.ts b/code/lib/cli/src/autoblock/block-storystorev6.ts index 9b2a0bd487f2..40a9f8822ac9 100644 --- a/code/lib/cli/src/autoblock/block-storystorev6.ts +++ b/code/lib/cli/src/autoblock/block-storystorev6.ts @@ -24,7 +24,7 @@ export const blocker = createBlocker({ StoryStoreV7 feature must be removed from your Storybook configuration. This feature was removed in Storybook 8.0.0. Please see the migration guide for more information: - https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#story-store-v7 + https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#storystorev6-and-storiesof-is-deprecated In your Storybook configuration file you have this code: diff --git a/code/lib/cli/src/autoblock/index.ts b/code/lib/cli/src/autoblock/index.ts index d5c812786747..ca8116d890cb 100644 --- a/code/lib/cli/src/autoblock/index.ts +++ b/code/lib/cli/src/autoblock/index.ts @@ -11,6 +11,7 @@ const blockers: () => BlockerModule[] = () => [ import('./block-storystorev6'), import('./block-stories-mdx'), import('./block-dependencies-versions'), + import('./block-node-version'), ]; type BlockerModule = Promise<{ blocker: Blocker }>; diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts index 53bf96534b31..e9f4a8151b1c 100644 --- a/code/lib/cli/src/upgrade.ts +++ b/code/lib/cli/src/upgrade.ts @@ -216,7 +216,7 @@ export const doUpgrade = async ({ !results && typeof mainConfig !== 'boolean' && typeof mainConfigPath !== 'undefined' && - options.force + !options.force ) { const blockResult = await autoblock({ packageManager,