Skip to content

Commit

Permalink
add sb doctor command
Browse files Browse the repository at this point in the history
  • Loading branch information
yannbf committed Apr 24, 2023
1 parent b307e75 commit 536a350
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 94 deletions.
100 changes: 100 additions & 0 deletions code/lib/cli/src/automigrate/helpers/getDuplicatedDepsWarnings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
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/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',
'@storybook/manager-api',
];

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

export function getDuplicatedDepsWarnings(
installationMetadata?: InstallationMetadata
): string[] | undefined {
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, 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>`
)}`
);

return messages;
}
99 changes: 6 additions & 93 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 './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,88 +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',
'@storybook/channel-websocket',
'@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',
'@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>`
)}`
);

return messages;
}
123 changes: 123 additions & 0 deletions code/lib/cli/src/doctor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import chalk from 'chalk';
import boxen from 'boxen';
import { createWriteStream, move, remove } from 'fs-extra';
import tempy from 'tempy';
import dedent from 'ts-dedent';

import { join } from 'path';
import { JsPackageManagerFactory } from '../js-package-manager';
import type { PackageManagerName } from '../js-package-manager';

import { getStorybookData } from '../automigrate/helpers/mainConfigFile';
import { getDuplicatedDepsWarnings } from '../automigrate/helpers/getDuplicatedDepsWarnings';
import { cleanLog } from '../automigrate/helpers/cleanLog';
import { getIncompatibleAddons } from '../automigrate/helpers/getIncompatibleAddons';
import { incompatibleAddons } from '../automigrate/fixes/incompatible-addons';

const logger = console;
const LOG_FILE_NAME = 'doctor-storybook.log';
const LOG_FILE_PATH = join(process.cwd(), LOG_FILE_NAME);
let TEMP_LOG_FILE_PATH = '';

const originalStdOutWrite = process.stdout.write.bind(process.stdout);
const originalStdErrWrite = process.stderr.write.bind(process.stdout);

const augmentLogsToFile = () => {
TEMP_LOG_FILE_PATH = tempy.file({ name: LOG_FILE_NAME });
const logStream = createWriteStream(TEMP_LOG_FILE_PATH);

process.stdout.write = (d: string) => {
originalStdOutWrite(d);
return logStream.write(cleanLog(d));
};
process.stderr.write = (d: string) => {
return logStream.write(cleanLog(d));
};
};

const cleanup = () => {
process.stdout.write = originalStdOutWrite;
process.stderr.write = originalStdErrWrite;
};

type DoctorOptions = {
configDir?: string;
packageManager?: PackageManagerName;
};

export const doctor = async ({
configDir: userSpecifiedConfigDir,
packageManager: pkgMgr,
}: DoctorOptions = {}) => {
augmentLogsToFile();
const diagnosticMessages: string[] = [];

logger.info('🩺 checking the health of your Storybook..');

const packageManager = JsPackageManagerFactory.getPackageManager({ force: pkgMgr });
let storybookVersion;
let mainConfig;

try {
const storybookData = await getStorybookData({
configDir: userSpecifiedConfigDir,
packageManager,
});
storybookVersion = storybookData.storybookVersion;
mainConfig = storybookData.mainConfig;
} catch (err) {
if (err.message.includes('No configuration files have been found')) {
logger.info(
dedent`[Storybook automigrate] Could not find or evaluate your Storybook main.js config directory at ${chalk.blue(
userSpecifiedConfigDir || '.storybook'
)} so the doctor command cannot proceed. You might be running this command in a monorepo or a non-standard project structure. If that is the case, please rerun this command by specifying the path to your Storybook config directory via the --config-dir option.`
);
}
logger.info(dedent`[Storybook doctor] ❌ ${err.message}`);
logger.info('Please fix the error and try again.');
}

if (!storybookVersion) {
logger.info(dedent`
[Storybook automigrate] ❌ Unable to determine storybook version so the automigrations will be skipped.
🤔 Are you running automigrate from your project directory? Please specify your Storybook config directory with the --config-dir flag.
`);
process.exit(1);
}

const installationMetadata = await packageManager.findInstallations([
'@storybook/*',
'storybook',
]);

diagnosticMessages.push(getDuplicatedDepsWarnings(installationMetadata)?.join('\n'));

const incompatibleAddonList = await getIncompatibleAddons(mainConfig);
if (incompatibleAddonList.length > 0) {
diagnosticMessages.push(incompatibleAddons.prompt({ incompatibleAddonList }));
}

logger.info();

const finalMessages = diagnosticMessages.filter(Boolean);

if (finalMessages.length > 0) {
finalMessages.push(`You can find the full logs in ${chalk.cyan(LOG_FILE_PATH)}`);

logger.info(
boxen(finalMessages.join('\n\n-------\n\n'), {
borderStyle: 'round',
padding: 1,
title: 'Diagnostics',
borderColor: 'red',
})
);
await move(TEMP_LOG_FILE_PATH, join(process.cwd(), LOG_FILE_NAME), { overwrite: true });
} else {
logger.info('🥳 Your Storybook project looks good!');
await remove(TEMP_LOG_FILE_PATH);
}
logger.info();

cleanup();
};
14 changes: 13 additions & 1 deletion code/lib/cli/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { dev } from './dev';
import { build } from './build';
import { parseList, getEnvConfig } from './utils';
import versions from './versions';
import { doctor } from './doctor';

addToGlobalContext('cliVersion', versions.storybook);

Expand Down Expand Up @@ -162,7 +163,7 @@ command('link <repo-url-or-directory>')
);

command('automigrate [fixId]')
.description('Check storybook for known problems or migrations and apply fixes')
.description('Check storybook for incompatibilities or migrations and apply fixes')
.option('-y --yes', 'Skip prompting the user')
.option('-n --dry-run', 'Only check for fixes, do not actually run them')
.option('--package-manager <npm|pnpm|yarn1|yarn2>', 'Force package manager')
Expand All @@ -181,6 +182,17 @@ command('automigrate [fixId]')
});
});

command('doctor')
.description('Check storybook for known problems and provide suggestions or fixes')
.option('--package-manager <npm|pnpm|yarn1|yarn2>', 'Force package manager')
.option('-c, --config-dir <dir-name>', 'Directory of Storybook configurations to migrate')
.action(async (options) => {
await doctor(options).catch((e) => {
logger.error(e);
process.exit(1);
});
});

command('dev')
.option('-p, --port <number>', 'Port to run Storybook', (str) => parseInt(str, 10))
.option('-h, --host <string>', 'Host to run Storybook')
Expand Down

0 comments on commit 536a350

Please sign in to comment.