Skip to content

Commit

Permalink
Merge pull request #22236 from storybookjs/feat/sb-doctor-command
Browse files Browse the repository at this point in the history
CLI: Add "doctor" command
  • Loading branch information
yannbf authored Nov 22, 2023
2 parents 2aa45f3 + 23dcb6b commit d2ef359
Show file tree
Hide file tree
Showing 12 changed files with 424 additions and 112 deletions.
35 changes: 34 additions & 1 deletion code/lib/cli/src/automigrate/fixes/incompatible-addons.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const check = async ({
describe('incompatible-addons fix', () => {
afterEach(jest.restoreAllMocks);

it('should show incompatible addons', async () => {
it('should show incompatible addons registered in main.js', async () => {
await expect(
check({
packageManager: {
Expand All @@ -38,6 +38,7 @@ describe('incompatible-addons fix', () => {
return Promise.resolve(null);
}
},
getAllDependencies: async () => ({}),
},
main: { addons: ['@storybook/essentials', '@storybook/addon-info'] },
})
Expand All @@ -51,6 +52,37 @@ describe('incompatible-addons fix', () => {
});
});

it('should show incompatible addons from package.json', async () => {
await expect(
check({
packageManager: {
getPackageVersion(packageName, basePath) {
switch (packageName) {
case '@storybook/addon-essentials':
return Promise.resolve('7.0.0');
case '@storybook/addon-info':
return Promise.resolve('5.3.21');
default:
return Promise.resolve(null);
}
},
getAllDependencies: async () => ({
'@storybook/addon-essentials': '7.0.0',
'@storybook/addon-info': '5.3.21',
}),
},
main: { addons: [] },
})
).resolves.toEqual({
incompatibleAddonList: [
{
name: '@storybook/addon-info',
version: '5.3.21',
},
],
});
});

it('no-op when there are no incompatible addons', async () => {
await expect(
check({
Expand All @@ -63,6 +95,7 @@ describe('incompatible-addons fix', () => {
return Promise.resolve(null);
}
},
getAllDependencies: async () => ({}),
},
main: { addons: ['@storybook/essentials'] },
})
Expand Down
4 changes: 2 additions & 2 deletions code/lib/cli/src/automigrate/fixes/incompatible-addons.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import chalk from 'chalk';
import dedent from 'ts-dedent';
import type { Fix } from '../types';
import { getIncompatibleAddons } from '../helpers/getIncompatibleAddons';
import { getIncompatibleAddons } from '../../doctor/getIncompatibleAddons';

interface IncompatibleAddonsOptions {
incompatibleAddonList: { name: string; version: string }[];
Expand All @@ -19,7 +19,7 @@ export const incompatibleAddons: Fix<IncompatibleAddonsOptions> = {
prompt({ incompatibleAddonList }) {
return dedent`
${chalk.bold(
chalk.red('Attention')
'Attention'
)}: We've detected that you're using the following addons in versions which are known to be incompatible with Storybook 7:
${incompatibleAddonList
Expand Down
13 changes: 11 additions & 2 deletions code/lib/cli/src/automigrate/helpers/getMigrationSummary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,25 @@ describe('getMigrationSummary', () => {
@storybook/instrumenter:
6.0.0, 7.1.0
Attention: The following dependencies are duplicated which might cause unexpected behavior:
@storybook/core-common:
6.0.0, 7.1.0
Attention: The following dependencies are duplicated which might cause unexpected behavior:
@storybook/addon-essentials:
7.0.0, 7.1.0
You can find more information for a given dependency by running yarn why <package-name>
Please try de-duplicating these dependencies by running yarn dedupe"
`);
});
Expand Down
104 changes: 6 additions & 98 deletions code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import chalk from 'chalk';
import boxen from 'boxen';
import { frameworkPackages, rendererPackages } from '@storybook/core-common';
import dedent from 'ts-dedent';
import type { FixSummary } from '../types';
import { FixStatus } from '../types';
import { hasMultipleVersions } from './hasMultipleVersions';
import type { InstallationMetadata } from '../../js-package-manager/types';
import { getDuplicatedDepsWarnings } from '../../doctor/getDuplicatedDepsWarnings';

const messageDivider = '\n\n';
export const messageDivider = '\n\n';
const segmentDivider = '\n\n─────────────────────────────────────────────────\n\n';

function getGlossaryMessages(
Expand Down Expand Up @@ -76,11 +75,10 @@ export function getMigrationSummary({
And reach out on Discord if you need help: ${chalk.yellow('https://discord.gg/storybook')}
`);

if (
installationMetadata?.duplicatedDependencies &&
Object.keys(installationMetadata.duplicatedDependencies).length > 0
) {
messages.push(getWarnings(installationMetadata).join(messageDivider));
const duplicatedDepsMessage = getDuplicatedDepsWarnings(installationMetadata);

if (duplicatedDepsMessage) {
messages.push(duplicatedDepsMessage.join(messageDivider));
}

const hasNoFixes = Object.values(fixResults).every((r) => r === FixStatus.UNNECESSARY);
Expand All @@ -102,93 +100,3 @@ export function getMigrationSummary({
borderColor: hasFailures ? 'red' : 'green',
});
}

// These packages are aliased by Storybook, so it doesn't matter if they're duplicated
const allowList = [
'@storybook/csf',
// see this file for more info: code/lib/preview/src/globals/types.ts
'@storybook/addons',
'@storybook/channel-postmessage', // @deprecated: remove in 8.0
'@storybook/channel-websocket', // @deprecated: remove in 8.0
'@storybook/channels',
'@storybook/client-api',
'@storybook/client-logger',
'@storybook/core-client',
'@storybook/core-events',
'@storybook/preview-web',
'@storybook/preview-api',
'@storybook/store',

// see this file for more info: code/ui/manager/src/globals/types.ts
'@storybook/components',
'@storybook/router',
'@storybook/theming',
'@storybook/api', // @deprecated: remove in 8.0
'@storybook/manager-api',
];

// These packages definitely will cause issues if they're duplicated
const disallowList = [
Object.keys(rendererPackages),
Object.keys(frameworkPackages),
'@storybook/instrumenter',
];

function getWarnings(installationMetadata: InstallationMetadata) {
const messages = [];

const { critical, trivial } = Object.entries(
installationMetadata?.duplicatedDependencies
).reduce<{
critical: string[];
trivial: string[];
}>(
(acc, [dep, versions]) => {
if (allowList.includes(dep)) {
return acc;
}

const hasMultipleMajorVersions = hasMultipleVersions(versions);

if (disallowList.includes(dep) && hasMultipleMajorVersions) {
acc.critical.push(`${chalk.redBright(dep)}:\n${versions.join(', ')}`);
} else {
acc.trivial.push(`${chalk.hex('#ff9800')(dep)}:\n${versions.join(', ')}`);
}

return acc;
},
{ critical: [], trivial: [] }
);

if (critical.length > 0) {
messages.push(
`${chalk.bold(
'Critical:'
)} The following dependencies are duplicated and WILL cause unexpected behavior:`
);
messages.push(critical.join(messageDivider));
}

if (trivial.length > 0) {
messages.push(
`${chalk.bold(
'Attention:'
)} The following dependencies are duplicated which might cause unexpected behavior:`
);
messages.push(trivial.join(messageDivider));
}

messages.push(
`You can find more information for a given dependency by running ${chalk.cyan(
`${installationMetadata.infoCommand} <package-name>`
)}`
);
messages.push(
`Please try de-duplicating these dependencies by running ${chalk.cyan(
`${installationMetadata.dedupeCommand}`
)}`
);

return messages;
}
119 changes: 119 additions & 0 deletions code/lib/cli/src/doctor/getDuplicatedDepsWarnings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import chalk from 'chalk';
import { frameworkPackages, rendererPackages } from '@storybook/core-common';
import { hasMultipleVersions } from './hasMultipleVersions';
import type { InstallationMetadata } from '../js-package-manager/types';

export const messageDivider = '\n\n';

// These packages are aliased by Storybook, so it doesn't matter if they're duplicated
export const allowList = [
'@storybook/csf',
// see this file for more info: code/lib/preview/src/globals/types.ts
'@storybook/addons',
'@storybook/channel-postmessage',
'@storybook/channel-websocket',
'@storybook/client-api',
'@storybook/client-logger',
'@storybook/core-client',
'@storybook/preview-web',
'@storybook/preview-api',
'@storybook/store',

// see this file for more info: code/ui/manager/src/globals/types.ts
'@storybook/components',
'@storybook/router',
'@storybook/theming',
'@storybook/api',
'@storybook/manager-api',
];

// These packages definitely will cause issues if they're duplicated
export const disallowList = [
Object.keys(rendererPackages),
Object.keys(frameworkPackages),
'@storybook/core-events',
'@storybook/instrumenter',
'@storybook/core-common',
'@storybook/core-server',
'@storybook/manager',
'@storybook/preview',
];

export function getDuplicatedDepsWarnings(
installationMetadata?: InstallationMetadata
): string[] | undefined {
try {
if (
!installationMetadata?.duplicatedDependencies ||
Object.keys(installationMetadata.duplicatedDependencies).length === 0
) {
return undefined;
}

const messages: string[] = [];

const { critical, trivial } = Object.entries(
installationMetadata?.duplicatedDependencies
).reduce<{
critical: string[];
trivial: string[];
}>(
(acc, [dep, packageVersions]) => {
if (allowList.includes(dep)) {
return acc;
}

const hasMultipleMajorVersions = hasMultipleVersions(packageVersions);

if (disallowList.includes(dep) && hasMultipleMajorVersions) {
acc.critical.push(`${chalk.redBright(dep)}:\n${packageVersions.join(', ')}`);
} else {
acc.trivial.push(`${chalk.hex('#ff9800')(dep)}:\n${packageVersions.join(', ')}`);
}

return acc;
},
{ critical: [], trivial: [] }
);

if (critical.length === 0 && trivial.length === 0) {
return messages;
}

if (critical.length > 0) {
messages.push(
`${chalk.bold(
'Critical:'
)} The following dependencies are duplicated and WILL cause unexpected behavior:`
);
messages.push(critical.join(messageDivider), '\n');
}

if (trivial.length > 0) {
messages.push(
`${chalk.bold(
'Attention:'
)} The following dependencies are duplicated which might cause unexpected behavior:`
);
messages.push(trivial.join(messageDivider));
}

messages.push(
'\n',
`You can find more information for a given dependency by running ${chalk.cyan(
`${installationMetadata.infoCommand} <package-name>`
)}`
);

messages.push(
'\n',
`Please try de-duplicating these dependencies by running ${chalk.cyan(
`${installationMetadata.dedupeCommand}`
)}`
);

return messages;
} catch (err) {
return undefined;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { StorybookConfig } from '@storybook/types';
import semver from 'semver';
import { getAddonNames } from './mainConfigFile';
import { JsPackageManagerFactory } from '../../js-package-manager';
import { getAddonNames } from '../automigrate/helpers/mainConfigFile';
import { JsPackageManagerFactory } from '../js-package-manager';

export const getIncompatibleAddons = async (
mainConfig: StorybookConfig,
Expand Down Expand Up @@ -38,12 +38,13 @@ export const getIncompatibleAddons = async (

const addons = getAddonNames(mainConfig).filter((addon) => addon in incompatibleList);

if (addons.length === 0) {
return [];
}
const dependencies = await packageManager.getAllDependencies();
const storybookPackages = Object.keys(dependencies).filter((dep) => dep.includes('storybook'));

const packagesToCheck = [...new Set([...addons, ...storybookPackages])];

const addonVersions = await Promise.all(
addons.map(
packagesToCheck.map(
async (addon) =>
({
name: addon,
Expand All @@ -52,6 +53,10 @@ export const getIncompatibleAddons = async (
)
);

if (addonVersions.length === 0) {
return [];
}

const incompatibleAddons: { name: string; version: string }[] = [];
addonVersions.forEach(({ name, version: installedVersion }) => {
if (installedVersion === null) return;
Expand Down
Loading

0 comments on commit d2ef359

Please sign in to comment.