Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: Add upgrade utility with version consistency check #11396

Merged
merged 2 commits into from
Jul 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/cli/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import initiate from './initiate';
import { add } from './add';
import { migrate } from './migrate';
import { extract } from './extract';
import { upgrade } from './upgrade';

// Cannot be `import` as it's not under TS root dir
const pkg = require('../package.json');
Expand All @@ -32,6 +33,15 @@ program
.option('-s --skip-postinstall', 'Skip package specific postinstall config modifications')
.action((addonName, options) => add(addonName, options));

program
.command('upgrade')
.description('Upgrade your storybook packages to the latest')
.option('-N --use-npm', 'Use NPM to build the Storybook server')
.option('-n --dry-run', 'Only check for upgrades, do not install')
.option('-p --prerelease', 'Upgrade to the pre-release packages')
.option('-s --skip-check', 'Skip postinstall version consistency checks')
.action((options) => upgrade(options));

program
.command('info')
.description('Prints debugging information about the local environment')
Expand Down
36 changes: 36 additions & 0 deletions lib/cli/src/upgrade.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { getStorybookVersion, isCorePackage } from './upgrade';

describe.each([
['│ │ │ ├── @babel/[email protected] deduped', null],
[
'│ ├── @storybook/[email protected] extraneous',
{ package: '@storybook/theming', version: '6.0.0-beta.37' },
],
[
'├─┬ @storybook/[email protected]',
{ package: '@storybook/preset-create-react-app', version: '3.1.2' },
],
['│ ├─┬ @storybook/[email protected]', { package: '@storybook/node-logger', version: '5.3.19' }],
[
'npm ERR! peer dep missing: @storybook/react@>=5.2, required by @storybook/[email protected]',
null,
],
])('getStorybookVersion', (input, output) => {
it(input, () => {
expect(getStorybookVersion(input)).toEqual(output);
});
});
Comment on lines +3 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused what this is testing

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the output of npm ls

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see.


describe.each([
['@storybook/react', true],
['@storybook/node-logger', true],
['@storybook/addon-info', true],
['@storybook/something-random', true],
['@storybook/preset-create-react-app', false],
['@storybook/linter-config', false],
['@storybook/design-system', false],
])('isCorePackage', (input, output) => {
it(input, () => {
expect(isCorePackage(input)).toEqual(output);
});
});
110 changes: 110 additions & 0 deletions lib/cli/src/upgrade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { sync as spawnSync } from 'cross-spawn';
import semver from '@storybook/semver';
import { logger } from '@storybook/node-logger';
import { JsPackageManagerFactory } from './js-package-manager';
import { commandLog } from './helpers';

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 excludeList = [
'@storybook/linter-config',
'@storybook/design-system',
'@storybook/ember-cli-storybook',
'@storybook/semver',
'@storybook/eslint-config-storybook',
'@storybook/addon-console',
'@storybook/csf',
];
export const isCorePackage = (pkg: string) =>
pkg.startsWith('@storybook/') &&
!pkg.startsWith('@storybook/preset-') &&
!excludeList.includes(pkg);

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' }).output.toString().split('\n');
const storybookPackages = lines
.map(getStorybookVersion)
.filter(Boolean)
.filter((pkg) => isCorePackage(pkg.package));
if (!storybookPackages.length) {
throw new Error('No storybook core packages found!');
}
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);
}
}
});
};

type Options = { prerelease: boolean; skipCheck: boolean; useNpm: boolean; dryRun: boolean };
export const upgrade = async ({ prerelease, skipCheck, useNpm, dryRun }: Options) => {
const packageManager = JsPackageManagerFactory.getPackageManager(useNpm);

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

const flags = [];
if (!dryRun) flags.push('--upgrade');
if (prerelease) flags.push('--newest');
const check = spawnSync('npx', ['npm-check-updates', '/storybook/', ...flags], {
stdio: 'pipe',
}).output.toString();
logger.info(check);

if (!dryRun) {
commandLog(`Installing upgrades`);
packageManager.installDependencies();
}

if (!skipCheck) checkVersionConsistency();
};