mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-07 21:31:48 +08:00
287 lines
8.9 KiB
TypeScript
287 lines
8.9 KiB
TypeScript
import { sync as spawnSync } from 'cross-spawn';
|
|
import { telemetry } from '@storybook/telemetry';
|
|
import semver, { eq, lt, prerelease } from 'semver';
|
|
import { logger } from '@storybook/node-logger';
|
|
import { withTelemetry } from '@storybook/core-server';
|
|
import {
|
|
UpgradeStorybookToLowerVersionError,
|
|
UpgradeStorybookToSameVersionError,
|
|
UpgradeStorybookUnknownCurrentVersionError,
|
|
} from '@storybook/core-events/server-errors';
|
|
|
|
import chalk from 'chalk';
|
|
import dedent from 'ts-dedent';
|
|
import boxen from 'boxen';
|
|
import type { JsPackageManager, PackageManagerName } from '@storybook/core-common';
|
|
import {
|
|
isCorePackage,
|
|
versions,
|
|
getStorybookInfo,
|
|
loadMainConfig,
|
|
JsPackageManagerFactory,
|
|
} from '@storybook/core-common';
|
|
import { automigrate } from './automigrate/index';
|
|
import { autoblock } from './autoblock/index';
|
|
|
|
type Package = {
|
|
package: string;
|
|
version: string;
|
|
};
|
|
|
|
const versionRegex = /(@storybook\/[^@]+)@(\S+)/;
|
|
export const getStorybookVersion = (line: string) => {
|
|
if (line.startsWith('npm ')) return null;
|
|
const match = versionRegex.exec(line);
|
|
if (!match || !semver.clean(match[2])) return null;
|
|
return {
|
|
package: match[1],
|
|
version: match[2],
|
|
};
|
|
};
|
|
|
|
const getInstalledStorybookVersion = async (packageManager: JsPackageManager) => {
|
|
const installations = await packageManager.findInstallations(Object.keys(versions));
|
|
if (!installations) {
|
|
return;
|
|
}
|
|
|
|
return Object.entries(installations.dependencies)[0]?.[1]?.[0].version;
|
|
};
|
|
|
|
const deprecatedPackages = [
|
|
{
|
|
minVersion: '6.0.0-alpha.0',
|
|
url: 'https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#60-deprecations',
|
|
deprecations: [
|
|
'@storybook/addon-notes',
|
|
'@storybook/addon-info',
|
|
'@storybook/addon-contexts',
|
|
'@storybook/addon-options',
|
|
'@storybook/addon-centered',
|
|
],
|
|
},
|
|
];
|
|
|
|
const formatPackage = (pkg: Package) => `${pkg.package}@${pkg.version}`;
|
|
|
|
const warnPackages = (pkgs: Package[]) =>
|
|
pkgs.forEach((pkg) => logger.warn(`- ${formatPackage(pkg)}`));
|
|
|
|
export const checkVersionConsistency = () => {
|
|
const lines = spawnSync('npm', ['ls'], { stdio: 'pipe', shell: true })
|
|
.output.toString()
|
|
.split('\n');
|
|
const storybookPackages = lines
|
|
.map(getStorybookVersion)
|
|
.filter((item): item is NonNullable<typeof item> => !!item)
|
|
.filter((pkg) => isCorePackage(pkg.package));
|
|
if (!storybookPackages.length) {
|
|
logger.warn('No storybook core packages found.');
|
|
logger.warn(`'npm ls | grep storybook' can show if multiple versions are installed.`);
|
|
return;
|
|
}
|
|
storybookPackages.sort((a, b) => semver.rcompare(a.version, b.version));
|
|
const latestVersion = storybookPackages[0].version;
|
|
const outdated = storybookPackages.filter((pkg) => pkg.version !== latestVersion);
|
|
if (outdated.length > 0) {
|
|
logger.warn(
|
|
`Found ${outdated.length} outdated packages (relative to '${formatPackage(
|
|
storybookPackages[0]
|
|
)}')`
|
|
);
|
|
logger.warn('Please make sure your packages are updated to ensure a consistent experience.');
|
|
warnPackages(outdated);
|
|
}
|
|
|
|
deprecatedPackages.forEach(({ minVersion, url, deprecations }) => {
|
|
if (semver.gte(latestVersion, minVersion)) {
|
|
const deprecated = storybookPackages.filter((pkg) => deprecations.includes(pkg.package));
|
|
if (deprecated.length > 0) {
|
|
logger.warn(`Found ${deprecated.length} deprecated packages since ${minVersion}`);
|
|
logger.warn(`See ${url}`);
|
|
warnPackages(deprecated);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
export interface UpgradeOptions {
|
|
skipCheck: boolean;
|
|
packageManager?: PackageManagerName;
|
|
dryRun: boolean;
|
|
yes: boolean;
|
|
force: boolean;
|
|
disableTelemetry: boolean;
|
|
configDir?: string;
|
|
}
|
|
|
|
export const doUpgrade = async ({
|
|
skipCheck,
|
|
packageManager: packageManagerName,
|
|
dryRun,
|
|
configDir: userSpecifiedConfigDir,
|
|
yes,
|
|
...options
|
|
}: UpgradeOptions) => {
|
|
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') ||
|
|
beforeVersion.startsWith('portal:') ||
|
|
beforeVersion.startsWith('workspace:');
|
|
|
|
if (!isCanary && lt(currentVersion, beforeVersion)) {
|
|
throw new UpgradeStorybookToLowerVersionError({ beforeVersion, currentVersion });
|
|
}
|
|
if (!isCanary && eq(currentVersion, beforeVersion)) {
|
|
throw new UpgradeStorybookToSameVersionError({ beforeVersion });
|
|
}
|
|
|
|
const [latestVersion, packageJson] = await Promise.all([
|
|
//
|
|
packageManager.latestVersion('@storybook/cli'),
|
|
packageManager.retrievePackageJson(),
|
|
]);
|
|
|
|
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.'),
|
|
};
|
|
|
|
logger.plain(
|
|
boxen(
|
|
[messages.welcome]
|
|
.concat(isOutdated && !isPrerelease ? [messages.notLatest] : [])
|
|
.concat(isPrerelease ? [messages.prelease] : [])
|
|
.join('\n'),
|
|
{ borderStyle: 'round', padding: 1, borderColor }
|
|
)
|
|
);
|
|
|
|
let results;
|
|
|
|
const { configDir: inferredConfigDir, mainConfig: mainConfigPath } = getStorybookInfo(
|
|
packageJson,
|
|
userSpecifiedConfigDir
|
|
);
|
|
const configDir = userSpecifiedConfigDir || inferredConfigDir || '.storybook';
|
|
|
|
const mainConfig = await loadMainConfig({ configDir });
|
|
|
|
// GUARDS
|
|
if (!beforeVersion) {
|
|
throw new UpgradeStorybookUnknownCurrentVersionError();
|
|
}
|
|
|
|
// BLOCKERS
|
|
if (
|
|
!results &&
|
|
typeof mainConfig !== 'boolean' &&
|
|
typeof mainConfigPath !== 'undefined' &&
|
|
!options.force
|
|
) {
|
|
const blockResult = await autoblock({
|
|
packageManager,
|
|
configDir,
|
|
packageJson,
|
|
mainConfig,
|
|
mainConfigPath,
|
|
});
|
|
if (blockResult) {
|
|
results = { preCheckFailure: blockResult };
|
|
}
|
|
}
|
|
|
|
// INSTALL UPDATED DEPENDENCIES
|
|
if (!dryRun && !results) {
|
|
const toUpgradedDependencies = (deps: Record<string, any>) => {
|
|
const monorepoDependencies = Object.keys(deps || {}).filter((dependency) => {
|
|
// only upgrade packages that are in the monorepo
|
|
return dependency in versions;
|
|
}) as Array<keyof typeof versions>;
|
|
return monorepoDependencies.map((dependency) => {
|
|
let char = '^';
|
|
if (isOutdated) {
|
|
char = '';
|
|
}
|
|
if (isCanary) {
|
|
char = '';
|
|
}
|
|
/* add ^ modifier to the version if this is the latest stable or prerelease version
|
|
example outputs: @storybook/react@^8.0.0 */
|
|
return `${dependency}@${char}${versions[dependency]}`;
|
|
});
|
|
};
|
|
|
|
const upgradedDependencies = toUpgradedDependencies(packageJson.dependencies);
|
|
const upgradedDevDependencies = toUpgradedDependencies(packageJson.devDependencies);
|
|
|
|
logger.info(`Updating dependencies in ${chalk.cyan('package.json')}..`);
|
|
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();
|
|
}
|
|
|
|
// AUTOMIGRATIONS
|
|
if (!skipCheck && !results && mainConfigPath) {
|
|
checkVersionConsistency();
|
|
results = await automigrate({
|
|
dryRun,
|
|
yes,
|
|
packageManager,
|
|
configDir,
|
|
mainConfigPath,
|
|
beforeVersion,
|
|
storybookVersion: currentVersion,
|
|
isUpgrade: true,
|
|
});
|
|
}
|
|
|
|
// TELEMETRY
|
|
if (!options.disableTelemetry) {
|
|
const { preCheckFailure, fixResults } = results || {};
|
|
const automigrationTelemetry = {
|
|
automigrationResults: preCheckFailure ? null : fixResults,
|
|
automigrationPreCheckFailure: preCheckFailure || null,
|
|
};
|
|
|
|
await telemetry('upgrade', {
|
|
beforeVersion,
|
|
afterVersion: currentVersion,
|
|
...automigrationTelemetry,
|
|
});
|
|
}
|
|
};
|
|
|
|
export async function upgrade(options: UpgradeOptions): Promise<void> {
|
|
await withTelemetry('upgrade', { cliOptions: options }, () => doUpgrade(options));
|
|
}
|