Skip to content

Commit

Permalink
new upgrade workflow based on #25553
Browse files Browse the repository at this point in the history
  • Loading branch information
JReinhold committed Jan 11, 2024
1 parent dbf6a9d commit 6f6ed73
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 157 deletions.
4 changes: 1 addition & 3 deletions code/lib/cli/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,14 @@ command('babelrc')
.action(() => generateStorybookBabelConfigInCWD());

command('upgrade')
.description('Upgrade your Storybook packages to the latest')
.description(`Upgrade your Storybook packages to v${versions.storybook}`)
.option(
'--package-manager <npm|pnpm|yarn1|yarn2>',
'Force package manager for installing dependencies'
)
.option('-N --use-npm', 'Use NPM to install dependencies (deprecated)')
.option('-y --yes', 'Skip prompting the user')
.option('-n --dry-run', 'Only check for upgrades, do not install')
.option('-t --tag <tag>', 'Upgrade to a certain npm dist-tag (e.g. next, prerelease)')
.option('-p --prerelease', 'Upgrade to the pre-release packages')
.option('-s --skip-check', 'Skip postinstall version and automigration checks')
.option('-c, --config-dir <dir-name>', 'Directory where to load Storybook configurations from')
.action(async (options: UpgradeOptions) => upgrade(options).catch(() => process.exit(1)));
Expand Down
2 changes: 2 additions & 0 deletions code/lib/cli/src/js-package-manager/JsPackageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ export abstract class JsPackageManager {

done = commandLog('Installing dependencies');

logger.log();

try {
await this.runInstall();
done();
Expand Down
87 changes: 25 additions & 62 deletions code/lib/cli/src/upgrade.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
import { addExtraFlags, addNxPackagesToReject, getStorybookVersion } from './upgrade';
import { getStorybookCoreVersion } from '@storybook/telemetry';
import {
UpgradeStorybookToLowerVersionError,
UpgradeStorybookToSameVersionError,
} from '@storybook/core-events/server-errors';
import { doUpgrade, getStorybookVersion } from './upgrade';

jest.mock('@storybook/telemetry');
jest.mock('./versions', () => {
const originalVersions = jest.requireActual('./versions').default;
return {
default: Object.keys(originalVersions).reduce((acc, key) => {
acc[key] = '8.0.0';
return acc;
}, {} as Record<string, string>),
};
});

describe.each([
['│ │ │ ├── @babel/[email protected] deduped', null],
Expand All @@ -21,68 +37,15 @@ describe.each([
});
});

describe('extra flags', () => {
const extraFlags = {
'react-scripts@<5': ['--foo'],
};
const devDependencies = {};
it('package matches constraints', () => {
expect(
addExtraFlags(extraFlags, [], { dependencies: { 'react-scripts': '4' }, devDependencies })
).toEqual(['--foo']);
});
it('package prerelease matches constraints', () => {
expect(
addExtraFlags(extraFlags, [], {
dependencies: { 'react-scripts': '4.0.0-alpha.0' },
devDependencies,
})
).toEqual(['--foo']);
});
it('package not matches constraints', () => {
expect(
addExtraFlags(extraFlags, [], {
dependencies: { 'react-scripts': '5.0.0-alpha.0' },
devDependencies,
})
).toEqual([]);
});
it('no package not matches constraints', () => {
expect(
addExtraFlags(extraFlags, [], {
dependencies: {},
devDependencies,
})
).toEqual([]);
});
});
describe('Upgrade errors', () => {
it('should throw an error when upgrading to a lower version number', async () => {
jest.mocked(getStorybookCoreVersion).mockResolvedValue('8.1.0');

describe('addNxPackagesToReject', () => {
it('reject exists and is in regex pattern', () => {
const flags = ['--reject', '/preset-create-react-app/', '--some-flag', 'hello'];
expect(addNxPackagesToReject(flags)).toMatchObject([
'--reject',
'/(preset-create-react-app|@nrwl/storybook|@nx/storybook)/',
'--some-flag',
'hello',
]);
await expect(doUpgrade({} as any)).rejects.toThrowError(UpgradeStorybookToLowerVersionError);
});
it('reject exists and is in unknown pattern', () => {
const flags = ['--some-flag', 'hello', '--reject', '@storybook/preset-create-react-app'];
expect(addNxPackagesToReject(flags)).toMatchObject([
'--some-flag',
'hello',
'--reject',
'@storybook/preset-create-react-app,@nrwl/storybook,@nx/storybook',
]);
});
it('reject does not exist', () => {
const flags = ['--some-flag', 'hello'];
expect(addNxPackagesToReject(flags)).toMatchObject([
'--some-flag',
'hello',
'--reject',
'@nrwl/storybook,@nx/storybook',
]);
it('should throw an error when upgrading to the same version number', async () => {
jest.mocked(getStorybookCoreVersion).mockResolvedValue('8.0.0');

await expect(doUpgrade({} as any)).rejects.toThrowError(UpgradeStorybookToSameVersionError);
});
});
178 changes: 86 additions & 92 deletions code/lib/cli/src/upgrade.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { sync as spawnSync } from 'cross-spawn';
import { telemetry, getStorybookCoreVersion } from '@storybook/telemetry';
import semver from 'semver';
import semver, { coerce, eq, lt, prerelease } from 'semver';
import { logger } from '@storybook/node-logger';
import { withTelemetry } from '@storybook/core-server';

import type { PackageJsonWithMaybeDeps, PackageManagerName } from './js-package-manager';
import { getPackageDetails, JsPackageManagerFactory, useNpmWarning } from './js-package-manager';
import {
UpgradeStorybookToLowerVersionError,
UpgradeStorybookToSameVersionError,
} from '@storybook/core-events/server-errors';

import chalk from 'chalk';
import dedent from 'ts-dedent';
import boxen from 'boxen';
import type { PackageManagerName } from './js-package-manager';
import { JsPackageManagerFactory, useNpmWarning } from './js-package-manager';
import { commandLog } from './helpers';
import { automigrate } from './automigrate';
import { isCorePackage } from './utils';
import versions from './versions';

type Package = {
package: string;
Expand Down Expand Up @@ -83,57 +91,7 @@ export const checkVersionConsistency = () => {
});
};

type ExtraFlags = Record<string, string[]>;
const EXTRA_FLAGS: ExtraFlags = {
'react-scripts@<5': ['--reject', '/preset-create-react-app/'],
};

export const addExtraFlags = (
extraFlags: ExtraFlags,
flags: string[],
{ dependencies, devDependencies }: PackageJsonWithMaybeDeps
) => {
return Object.entries(extraFlags).reduce(
(acc, entry) => {
const [pattern, extra] = entry;
const [pkg, specifier] = getPackageDetails(pattern);
const pkgVersion = dependencies[pkg] || devDependencies[pkg];

if (pkgVersion && semver.satisfies(semver.coerce(pkgVersion), specifier)) {
return [...acc, ...extra];
}

return acc;
},
[...flags]
);
};

export const addNxPackagesToReject = (flags: string[]) => {
const newFlags = [...flags];
const index = flags.indexOf('--reject');
if (index > -1) {
// Try to understand if it's in the format of a regex pattern
if (newFlags[index + 1].endsWith('/') && newFlags[index + 1].startsWith('/')) {
// Remove last and first slash so that I can add the parentheses
newFlags[index + 1] = newFlags[index + 1].substring(1, newFlags[index + 1].length - 1);
newFlags[index + 1] = `/(${newFlags[index + 1]}|@nrwl/storybook|@nx/storybook)/`;
} else {
// Adding the two packages as comma-separated values
// If the existing rejects are in regex format, they will be ignored.
// Maybe we need to find a more robust way to treat rejects?
newFlags[index + 1] = `${newFlags[index + 1]},@nrwl/storybook,@nx/storybook`;
}
} else {
newFlags.push('--reject');
newFlags.push('@nrwl/storybook,@nx/storybook');
}
return newFlags;
};

export interface UpgradeOptions {
tag: string;
prerelease: boolean;
skipCheck: boolean;
useNpm: boolean;
packageManager: PackageManagerName;
Expand All @@ -144,8 +102,6 @@ export interface UpgradeOptions {
}

export const doUpgrade = async ({
tag,
prerelease,
skipCheck,
useNpm,
packageManager: pkgMgr,
Expand All @@ -161,48 +117,88 @@ export const doUpgrade = async ({
}
const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr });

const currentVersion = versions['@storybook/cli'];
const beforeVersion = await getStorybookCoreVersion();

commandLog(`Checking for latest versions of '@storybook/*' packages`);

if (tag && prerelease) {
throw new Error(
`Cannot set both --tag and --prerelease. Use --tag next to get the latest prereleae`
);
if (lt(currentVersion, beforeVersion)) {
throw new UpgradeStorybookToLowerVersionError({ beforeVersion, currentVersion });
}

let target = 'latest';
if (prerelease) {
// '@next' is storybook's convention for the latest prerelease tag.
// This used to be 'greatest', but that was not reliable and could pick canaries, etc.
// and random releases of other packages with storybook in their name.
target = '@next';
} else if (tag) {
target = `@${tag}`;
if (eq(currentVersion, beforeVersion)) {
throw new UpgradeStorybookToSameVersionError({ beforeVersion });
}

let flags = [];
if (!dryRun) flags.push('--upgrade');
flags.push('--target');
flags.push(target);
flags = addExtraFlags(EXTRA_FLAGS, flags, await packageManager.retrievePackageJson());
flags = addNxPackagesToReject(flags);
const check = spawnSync('npx', ['npm-check-updates@latest', '/storybook/', ...flags], {
stdio: 'pipe',
shell: true,
});
logger.info(check.stdout.toString());
logger.info(check.stderr.toString());
const latestVersion = await packageManager.latestVersion('@storybook/cli');
const isOutdated = lt(currentVersion, latestVersion);
const isPrerelease = prerelease(currentVersion) !== null;

const borderColor = isOutdated ? '#FC521F' : '#F1618C';

const messages = {
welcome: `Upgrading Storybook from version ${chalk.bold(beforeVersion)} to version ${chalk.bold(
currentVersion
)}..`,
notLatest: chalk.red(dedent`
This version is behind the latest release, which is: ${chalk.bold(latestVersion)}!
You likely ran the upgrade command through npx, which can use a locally cached version, to upgrade to the latest version please run:
${chalk.bold('npx storybook@latest upgrade')}
You may want to CTRL+C to stop, and run with the latest version instead.
`),
prelease: chalk.yellow('This is a pre-release version.'),
};

const checkSb = spawnSync('npx', ['npm-check-updates@latest', 'sb', ...flags], {
stdio: 'pipe',
shell: true,
});
logger.info(checkSb.stdout.toString());
logger.info(checkSb.stderr.toString());
logger.plain(
boxen(
[messages.welcome]
.concat(isOutdated && !isPrerelease ? [messages.notLatest] : [])
.concat(isPrerelease ? [messages.prelease] : [])
.join('\n'),
{ borderStyle: 'round', padding: 1, borderColor }
)
);

const packageJson = await packageManager.retrievePackageJson();

const toUpgradedDependencies = (deps: Record<string, any>) => {
const monorepoDependencies = Object.keys(deps || {}).filter((dependency) => {
// don't upgrade @storybook/preset-create-react-app if react-scripts is < v5
if (dependency === '@storybook/preset-create-react-app') {
const reactScriptsVersion =
packageJson.dependencies['react-scripts'] ?? packageJson.devDependencies['react-scripts'];
if (reactScriptsVersion && lt(coerce(reactScriptsVersion), '5.0.0')) {
return false;
}
}

// only upgrade packages that are in the monorepo
return dependency in versions;
}) as Array<keyof typeof versions>;
return monorepoDependencies.map(
(dependency) =>
// add ^ modifier to the version if this is the latest and stable version
// example output: @storybook/react@^8.0.0
`${dependency}@${!isOutdated || isPrerelease ? '^' : ''}${versions[dependency]}`
);
};

const upgradedDependencies = toUpgradedDependencies(packageJson.dependencies);
const upgradedDevDependencies = toUpgradedDependencies(packageJson.devDependencies);

if (!dryRun) {
commandLog(`Installing upgrades`);
commandLog(`Updating dependencies in ${chalk.cyan('package.json')}..`);
logger.plain('');
if (upgradedDependencies.length > 0) {
await packageManager.addDependencies(
{ installAsDevDependencies: false, skipInstall: true, packageJson },
upgradedDependencies
);
}
if (upgradedDevDependencies.length > 0) {
await packageManager.addDependencies(
{ installAsDevDependencies: true, skipInstall: true, packageJson },
upgradedDevDependencies
);
}
await packageManager.installDependencies();
}

Expand All @@ -219,8 +215,6 @@ export const doUpgrade = async ({
automigrationPreCheckFailure: preCheckFailure || null,
};
telemetry('upgrade', {
prerelease,
tag,
beforeVersion,
afterVersion,
...automigrationTelemetry,
Expand Down
Loading

0 comments on commit 6f6ed73

Please sign in to comment.