improvements

This commit is contained in:
Norbert de Langen 2024-03-07 17:31:50 +01:00
parent 880479789f
commit 54eccd103d
3 changed files with 110 additions and 51 deletions

View File

@ -2,13 +2,13 @@ import { dedent } from 'ts-dedent';
import type { Fix } from '../types';
import { underline } from 'chalk';
import { getIncompatibleStorybookPackages } from '../../doctor/getIncompatibleStorybookPackages';
import { valid, coerce } from 'semver';
interface Options {
list: { packageName: string; version: string }[];
upgradable: { packageName: string; version: string }[];
problematicPackages: { packageName: string; version: string }[];
}
type ExcludesFalse = <T>(x: T | undefined) => x is T;
/**
* 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!
@ -20,36 +20,62 @@ type ExcludesFalse = <T>(x: T | undefined) => x is T;
*/
export const upgradeStorybookRelatedDependencies = {
id: 'upgradeStorybookRelatedDependencies',
versionRange: ['<8', '>=8'],
versionRange: ['*.*.*', '*.*.*'],
promptType: 'auto',
promptDefaultValue: false,
async check({ packageManager, storybookVersion }) {
const out = await getIncompatibleStorybookPackages({
const packageJson = await packageManager.readPackageJson();
const analyzed = await getIncompatibleStorybookPackages({
currentStorybookVersion: storybookVersion,
packageManager,
skipErrors: true,
});
const list = await Promise.all(
out.map(async ({ packageName, hasIncompatibleDependencies }) => {
if (!hasIncompatibleDependencies) {
return;
}
const all = await packageManager.getAllDependencies();
const associated = Object.keys(all).filter((dep) => dep.includes('storybook'));
const detected = analyzed
.filter((m) => m.hasIncompatibleDependencies)
.map((m) => m.packageName);
const list = await Promise.all(
Array.from(new Set([...associated, ...detected])).map(async (packageName) => {
return {
packageName,
version: await packageManager.latestVersion(packageName),
version: await packageManager.latestVersion(packageName).catch((e) => null),
};
})
);
const filtered = list.filter(Boolean as any as ExcludesFalse);
return { list: filtered };
const data = list.reduce<Options>(
(acc, k) => {
const upgradable = !(
!valid(k.version) ||
k.version === coerce(packageJson?.dependencies?.[k.packageName])?.toString() ||
k.version === coerce(packageJson?.devDependencies?.[k.packageName])?.toString() ||
k.version === coerce(packageJson?.peerDependencies?.[k.packageName])?.toString()
);
if (upgradable) {
acc.upgradable.push(k);
} else {
acc.problematicPackages.push(k);
}
return acc;
},
{ upgradable: [], problematicPackages: [] }
);
if (data.upgradable.length > 0) {
return data;
}
return null;
},
promptType: 'auto-no',
prompt({ list }) {
prompt({ upgradable: list }) {
return dedent`
You're upgrading to the latest version of Storybook. We recommend upgrading the following packages:
${list.map(({ packageName, version }) => `${packageName}@${version}`).join(', ')}
@ -66,40 +92,58 @@ export const upgradeStorybookRelatedDependencies = {
`;
},
async run({ result: { list }, packageManager, dryRun, mainConfigPath }) {
async run({ result: { upgradable, problematicPackages }, packageManager, dryRun }) {
if (dryRun) {
console.log(dedent`
would have upgrade the following:
${list.map(({ packageName, version }) => `${packageName}@${version}`).join('\n')}
We would have upgrade the following:
${upgradable.map(({ packageName, version }) => `${packageName}@${version}`).join('\n')}
`);
return;
}
const packageJson = await packageManager.readPackageJson();
if (upgradable.length > 0) {
const packageJson = await packageManager.readPackageJson();
// mutate the packageJson data
list.forEach((item) => {
if (!item) {
return;
}
upgradable.forEach((item) => {
if (!item) {
return;
}
const { packageName, version } = item;
const prefixed = `^${version}`;
const { packageName, 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;
}
});
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.writePackageJson(packageJson);
await packageManager.installDependencies();
await packageManager.getRunCommand('dedupe');
await packageManager
.executeCommand({ command: 'dedupe', args: [], stdio: 'ignore' })
.catch((e) => {});
console.log(dedent`
We upgraded ${upgradable.length} packages:
${upgradable.map(({ packageName, version }) => `- ${packageName}@${version}`).join('\n')}
`);
}
if (problematicPackages.length) {
console.log(dedent`
The following packages, could not be upgraded, likely because there's no update available that's compatible with the latest version of Storybook:
${problematicPackages.map(({ packageName }) => `- ${packageName}`).join('\n')}
We suggest your reach out to the authors of these packages to get them updated.
But before reporting, please check if there is already an open issue or PR for this.
`);
}
},
} satisfies Fix<Options>;

View File

@ -30,6 +30,7 @@ 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';
@ -58,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) => {
@ -86,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,
@ -96,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 ({
@ -127,7 +138,11 @@ export const automigrate = async ({
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') {
if (
fix.id === upgradeStorybookRelatedDependencies.id &&
isUpgrade !== 'latest' &&
fixId !== upgradeStorybookRelatedDependencies.id
) {
return false;
}
@ -319,13 +334,13 @@ export async function runFixes({
fixResults[f.id] = FixStatus.MANUAL_SKIPPED;
break;
}
} else if (promptType === 'auto' || promptType === 'auto-no') {
} else if (promptType === 'auto') {
runAnswer = await prompts(
{
type: 'confirm',
name: 'fix',
message: `Do you want to run the '${chalk.cyan(f.id)}' migration on your project?`,
initial: promptType === 'auto-no' ? false : true,
initial: f.promptDefaultValue ?? true,
},
{
onCancel: () => {

View File

@ -22,11 +22,10 @@ export interface RunOptions<ResultType> {
/**
* promptType defines how the user will be prompted to apply an automigration fix
* - auto: the fix will be applied automatically
* - auto-no: the fix will be applied automatically, but only when the user opts-in
* - manual: the user will be prompted to apply the fix
* - notification: the user will be notified about some changes. A fix isn't required, though
*/
export type Prompt = 'auto' | 'auto-no' | 'manual' | 'notification';
export type Prompt = 'auto' | 'manual' | 'notification';
type BaseFix<ResultType = any> = {
id: string;
@ -38,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> =
@ -46,7 +46,7 @@ type PromptType<ResultType = any, T = Prompt> =
export type Fix<ResultType = any> = (
| {
promptType?: PromptType<ResultType, 'auto' | 'auto-no'>;
promptType?: PromptType<ResultType, 'auto'>;
run: (options: RunOptions<ResultType>) => Promise<void>;
}
| {