Skip to content

Commit

Permalink
Merge pull request #26377 from storybookjs/norbert/automigration-upgr…
Browse files Browse the repository at this point in the history
…ading-storybook-related-deps

CLI: Automigration for upgrading storybook related dependencies
  • Loading branch information
ndelangen committed Mar 8, 2024
2 parents 0b2b5ff + b74ca12 commit 0cfd141
Show file tree
Hide file tree
Showing 6 changed files with 296 additions and 10 deletions.
2 changes: 2 additions & 0 deletions code/lib/cli/src/automigrate/fixes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { removeJestTestingLibrary } from './remove-jest-testing-library';
import { addonsAPI } from './addons-api';
import { mdx1to3 } from './mdx-1-to-3';
import { addonPostCSS } from './addon-postcss';
import { upgradeStorybookRelatedDependencies } from './upgrade-storybook-related-dependencies';

export * from '../types';

Expand Down Expand Up @@ -56,6 +57,7 @@ export const allFixes: Fix[] = [
removeLegacyMDX1,
webpack5CompilerSetup,
mdx1to3,
upgradeStorybookRelatedDependencies,
];

export const initFixes: Fix[] = [eslintPlugin];
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, afterEach, it, expect, vi } from 'vitest';
import type { StorybookConfig } from '@storybook/types';
import type { JsPackageManager } from '@storybook/core-common';
import * as docsUtils from '../../doctor/getIncompatibleStorybookPackages';

import { upgradeStorybookRelatedDependencies } from './upgrade-storybook-related-dependencies';

vi.mock('../../doctor/getIncompatibleStorybookPackages');

const check = async ({
packageManager,
main: mainConfig = {},
storybookVersion = '8.0.0',
}: {
packageManager: Partial<JsPackageManager>;
main?: Partial<StorybookConfig> & Record<string, unknown>;
storybookVersion?: string;
}) => {
return upgradeStorybookRelatedDependencies.check({
packageManager: packageManager as any,
configDir: '',
mainConfig: mainConfig as any,
storybookVersion,
});
};

describe('upgrade-storybook-related-dependencies fix', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it('should detect storyshots registered in main.js', async () => {
const analyzedPackages = [
{
packageName: '@chromatic-com/storybook',
packageVersion: '1.2.9',
availableUpgrade: '2.0.0',
hasIncompatibleDependencies: false,
},
{
packageName: '@storybook/jest',
packageVersion: '0.2.3',
availableUpgrade: '1.0.0',
hasIncompatibleDependencies: false,
},
{
packageName: '@storybook/preset-create-react-app',
packageVersion: '3.2.0',
availableUpgrade: '8.0.0',
hasIncompatibleDependencies: true,
},
{
packageName: 'storybook',
packageVersion: '8.0.0',
availableUpgrade: undefined,
hasIncompatibleDependencies: true,
},
];
vi.mocked(docsUtils.getIncompatibleStorybookPackages).mockResolvedValue(analyzedPackages);
await expect(
check({
packageManager: {
getAllDependencies: async () => ({
'@chromatic-com/storybook': '1.2.9',
'@storybook/jest': '0.2.3',
'@storybook/preset-create-react-app': '3.2.0',
storybook: '8.0.0',
}),
latestVersion: async (pkgName) =>
analyzedPackages.find((pkg) => pkg.packageName === pkgName)?.availableUpgrade || '',
},
})
).resolves.toMatchInlineSnapshot(`
{
"upgradable": [
{
"afterVersion": "2.0.0",
"beforeVersion": "1.2.9",
"packageName": "@chromatic-com/storybook",
},
{
"afterVersion": "1.0.0",
"beforeVersion": "0.2.3",
"packageName": "@storybook/jest",
},
{
"afterVersion": "8.0.0",
"beforeVersion": "3.2.0",
"packageName": "@storybook/preset-create-react-app",
},
],
}
`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { dedent } from 'ts-dedent';
import { cyan, yellow } from 'chalk';
import { valid, coerce } from 'semver';
import type { JsPackageManager } from '@storybook/core-common';
import { isCorePackage } from '@storybook/core-common';
import type { Fix } from '../types';
import { getIncompatibleStorybookPackages } from '../../doctor/getIncompatibleStorybookPackages';

type PackageMetadata = {
packageName: string;
beforeVersion: string | null;
afterVersion: string | null;
};

interface Options {
upgradable: PackageMetadata[];
}

async function getLatestVersions(
packageManager: JsPackageManager,
packages: [string, string][]
): Promise<PackageMetadata[]> {
return Promise.all(
packages.map(async ([packageName, beforeVersion]) => ({
packageName,
beforeVersion: coerce(beforeVersion)?.toString() || null,
afterVersion: await packageManager.latestVersion(packageName).catch(() => null),
}))
);
}

function isPackageUpgradable(
afterVersion: string,
packageName: string,
allDependencies: Record<string, string>
) {
const installedVersion = coerce(allDependencies[packageName])?.toString();

return valid(afterVersion) && afterVersion !== installedVersion;
}

/**
* Is the user upgrading to the `latest` version of Storybook?
* Let's try to pull along some of the storybook related dependencies to `latest` as well!
*
* We communicate clearly that this migration is a helping hand, but not a complete solution.
* The user should still manually check for other dependencies that might be incompatible.
*
* see: https://github.com/storybookjs/storybook/issues/25731#issuecomment-1977346398
*/
export const upgradeStorybookRelatedDependencies = {
id: 'upgradeStorybookRelatedDependencies',
versionRange: ['*.*.*', '*.*.*'],
promptType: 'auto',
promptDefaultValue: false,

async check({ packageManager, storybookVersion }) {
const analyzedPackages = await getIncompatibleStorybookPackages({
currentStorybookVersion: storybookVersion,
packageManager,
skipErrors: true,
});

const allDependencies = (await packageManager.getAllDependencies()) as Record<string, string>;
const storybookDependencies = Object.keys(allDependencies)
.filter((dep) => dep.includes('storybook'))
.filter((dep) => !isCorePackage(dep));
const incompatibleDependencies = analyzedPackages
.filter((pkg) => pkg.hasIncompatibleDependencies)
.map((pkg) => pkg.packageName);

const uniquePackages = Array.from(
new Set([...storybookDependencies, ...incompatibleDependencies])
).map((packageName) => [packageName, allDependencies[packageName]]) as [string, string][];

const packageVersions = await getLatestVersions(packageManager, uniquePackages);

const upgradablePackages = packageVersions.filter(
({ packageName, afterVersion, beforeVersion }) => {
if (beforeVersion === null || afterVersion === null) {
return false;
}

return isPackageUpgradable(afterVersion, packageName, allDependencies);
}
);

return upgradablePackages.length > 0 ? { upgradable: upgradablePackages } : null;
},

prompt({ upgradable }) {
return dedent`
You're upgrading to the latest version of Storybook. We recommend upgrading the following packages:
${upgradable
.map(({ packageName, afterVersion, beforeVersion }) => {
return `- ${cyan(packageName)}: ${cyan(beforeVersion)} => ${cyan(afterVersion)}`;
})
.join('\n')}
After upgrading, we will run the dedupe command, which could possibly have effects on dependencies that are not Storybook related.
see: https://docs.npmjs.com/cli/commands/npm-dedupe
Do you want to proceed (upgrade the detected packages)?
`;
},

async run({ result: { upgradable }, packageManager, dryRun }) {
if (dryRun) {
console.log(dedent`
We would have upgrade the following:
${upgradable
.map(
({ packageName, afterVersion, beforeVersion }) =>
`${packageName}: ${beforeVersion} => ${afterVersion}`
)
.join('\n')}
`);
return;
}

if (upgradable.length > 0) {
const packageJson = await packageManager.readPackageJson();

upgradable.forEach((item) => {
if (!item) {
return;
}

const { packageName, afterVersion: version } = item;
const prefixed = `^${version}`;

if (packageJson.dependencies?.[packageName]) {
packageJson.dependencies[packageName] = prefixed;
}
if (packageJson.devDependencies?.[packageName]) {
packageJson.devDependencies[packageName] = prefixed;
}
if (packageJson.peerDependencies?.[packageName]) {
packageJson.peerDependencies[packageName] = prefixed;
}
});

await packageManager.writePackageJson(packageJson);
await packageManager.installDependencies();

await packageManager
.executeCommand({ command: 'dedupe', args: [], stdio: 'ignore' })
.catch(() => {});

console.log();
console.log(dedent`
We upgraded ${yellow(upgradable.length)} packages:
${upgradable
.map(({ packageName, afterVersion, beforeVersion }) => {
return `- ${cyan(packageName)}: ${cyan(beforeVersion)} => ${cyan(afterVersion)}`;
})
.join('\n')}
`);
}
console.log();
},
} satisfies Fix<Options>;
42 changes: 34 additions & 8 deletions code/lib/cli/src/automigrate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import { getMigrationSummary } from './helpers/getMigrationSummary';
import { getStorybookData } from './helpers/mainConfigFile';
import { doctor } from '../doctor';

import { upgradeStorybookRelatedDependencies } from './fixes/upgrade-storybook-related-dependencies';
import dedent from 'ts-dedent';

const logger = console;
const LOG_FILE_NAME = 'migration-storybook.log';
const LOG_FILE_PATH = join(process.cwd(), LOG_FILE_NAME);
Expand Down Expand Up @@ -56,8 +59,16 @@ const cleanup = () => {
};

const logAvailableMigrations = () => {
const availableFixes = allFixes.map((f) => chalk.yellow(f.id)).join(', ');
logger.info(`\nThe following migrations are available: ${availableFixes}`);
const availableFixes = allFixes
.map((f) => chalk.yellow(f.id))
.map((x) => `- ${x}`)
.join('\n');

console.log();
logger.info(dedent`
The following migrations are available:
${availableFixes}
`);
};

export const doAutomigrate = async (options: AutofixOptionsFromCLI) => {
Expand All @@ -84,7 +95,7 @@ export const doAutomigrate = async (options: AutofixOptionsFromCLI) => {
throw new Error('Could not determine main config path');
}

await automigrate({
const outcome = await automigrate({
...options,
packageManager,
storybookVersion,
Expand All @@ -94,7 +105,9 @@ export const doAutomigrate = async (options: AutofixOptionsFromCLI) => {
isUpgrade: false,
});

await doctor({ configDir, packageManager: options.packageManager });
if (outcome) {
await doctor({ configDir, packageManager: options.packageManager });
}
};

export const automigrate = async ({
Expand All @@ -121,8 +134,21 @@ export const automigrate = async ({
return null;
}

const selectedFixes = inputFixes || allFixes;
const fixes = fixId ? selectedFixes.filter((f) => f.id === fixId) : selectedFixes;
const selectedFixes: Fix[] =
inputFixes ||
allFixes.filter((fix) => {
// we only allow this automigration when the user explicitly asks for it, or they are upgrading to the latest version of storybook
if (
fix.id === upgradeStorybookRelatedDependencies.id &&
isUpgrade !== 'latest' &&
fixId !== upgradeStorybookRelatedDependencies.id
) {
return false;
}

return true;
});
const fixes: Fix[] = fixId ? selectedFixes.filter((f) => f.id === fixId) : selectedFixes;

if (fixId && fixes.length === 0) {
logger.info(`📭 No migrations found for ${chalk.magenta(fixId)}.`);
Expand All @@ -143,7 +169,7 @@ export const automigrate = async ({
mainConfigPath,
storybookVersion,
beforeVersion,
isUpgrade,
isUpgrade: !!isUpgrade,
dryRun,
yes,
});
Expand Down Expand Up @@ -314,7 +340,7 @@ export async function runFixes({
type: 'confirm',
name: 'fix',
message: `Do you want to run the '${chalk.cyan(f.id)}' migration on your project?`,
initial: true,
initial: f.promptDefaultValue ?? true,
},
{
onCancel: () => {
Expand Down
3 changes: 2 additions & 1 deletion code/lib/cli/src/automigrate/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type BaseFix<ResultType = any> = {
versionRange: [from: string, to: string];
check: (options: CheckOptions) => Promise<ResultType | null>;
prompt: (result: ResultType) => string;
promptDefaultValue?: boolean;
};

type PromptType<ResultType = any, T = Prompt> =
Expand Down Expand Up @@ -74,7 +75,7 @@ export interface AutofixOptions extends Omit<AutofixOptionsFromCLI, 'packageMana
/**
* Whether the migration is part of an upgrade.
*/
isUpgrade: boolean;
isUpgrade: false | true | 'latest';
}
export interface AutofixOptionsFromCLI {
fixId?: FixId;
Expand Down
2 changes: 1 addition & 1 deletion code/lib/cli/src/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ export const doUpgrade = async ({
mainConfigPath,
beforeVersion,
storybookVersion: currentVersion,
isUpgrade: true,
isUpgrade: isOutdated ? true : 'latest',
});
}

Expand Down

0 comments on commit 0cfd141

Please sign in to comment.