diff --git a/code/lib/cli/src/automigrate/fixes/incompatible-addons.test.ts b/code/lib/cli/src/automigrate/fixes/incompatible-addons.test.ts index 46978eba8396..fd4705acf879 100644 --- a/code/lib/cli/src/automigrate/fixes/incompatible-addons.test.ts +++ b/code/lib/cli/src/automigrate/fixes/incompatible-addons.test.ts @@ -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: { @@ -38,6 +38,7 @@ describe('incompatible-addons fix', () => { return Promise.resolve(null); } }, + getAllDependencies: async () => ({}), }, main: { addons: ['@storybook/essentials', '@storybook/addon-info'] }, }) @@ -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({ @@ -63,6 +95,7 @@ describe('incompatible-addons fix', () => { return Promise.resolve(null); } }, + getAllDependencies: async () => ({}), }, main: { addons: ['@storybook/essentials'] }, }) diff --git a/code/lib/cli/src/automigrate/fixes/incompatible-addons.ts b/code/lib/cli/src/automigrate/fixes/incompatible-addons.ts index 3455408f279d..ce9d01ccd96a 100644 --- a/code/lib/cli/src/automigrate/fixes/incompatible-addons.ts +++ b/code/lib/cli/src/automigrate/fixes/incompatible-addons.ts @@ -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 }[]; @@ -19,7 +19,7 @@ export const incompatibleAddons: Fix = { 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 diff --git a/code/lib/cli/src/automigrate/helpers/getMigrationSummary.test.ts b/code/lib/cli/src/automigrate/helpers/getMigrationSummary.test.ts index f5569ae2f49d..8a28a6c4fe23 100644 --- a/code/lib/cli/src/automigrate/helpers/getMigrationSummary.test.ts +++ b/code/lib/cli/src/automigrate/helpers/getMigrationSummary.test.ts @@ -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 + + + Please try de-duplicating these dependencies by running yarn dedupe" `); }); diff --git a/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts b/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts index 6a16e2ae3291..583d1b9170ee 100644 --- a/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts +++ b/code/lib/cli/src/automigrate/helpers/getMigrationSummary.ts @@ -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( @@ -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); @@ -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} ` - )}` - ); - messages.push( - `Please try de-duplicating these dependencies by running ${chalk.cyan( - `${installationMetadata.dedupeCommand}` - )}` - ); - - return messages; -} diff --git a/code/lib/cli/src/doctor/getDuplicatedDepsWarnings.ts b/code/lib/cli/src/doctor/getDuplicatedDepsWarnings.ts new file mode 100644 index 000000000000..0015798988de --- /dev/null +++ b/code/lib/cli/src/doctor/getDuplicatedDepsWarnings.ts @@ -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} ` + )}` + ); + + messages.push( + '\n', + `Please try de-duplicating these dependencies by running ${chalk.cyan( + `${installationMetadata.dedupeCommand}` + )}` + ); + + return messages; + } catch (err) { + return undefined; + } +} diff --git a/code/lib/cli/src/automigrate/helpers/getIncompatibleAddons.ts b/code/lib/cli/src/doctor/getIncompatibleAddons.ts similarity index 84% rename from code/lib/cli/src/automigrate/helpers/getIncompatibleAddons.ts rename to code/lib/cli/src/doctor/getIncompatibleAddons.ts index d6fc28ed776b..135865f949c7 100644 --- a/code/lib/cli/src/automigrate/helpers/getIncompatibleAddons.ts +++ b/code/lib/cli/src/doctor/getIncompatibleAddons.ts @@ -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, @@ -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, @@ -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; diff --git a/code/lib/cli/src/doctor/getMismatchingVersionsWarning.ts b/code/lib/cli/src/doctor/getMismatchingVersionsWarning.ts new file mode 100644 index 000000000000..9aa0d424e01d --- /dev/null +++ b/code/lib/cli/src/doctor/getMismatchingVersionsWarning.ts @@ -0,0 +1,95 @@ +import chalk from 'chalk'; +import semver from 'semver'; +import { frameworkPackages } from '@storybook/core-common'; +import type { InstallationMetadata } from '../js-package-manager/types'; +import storybookCorePackages from '../versions'; + +function getPrimaryVersion(name: string, installationMetadata?: InstallationMetadata) { + const packageMetadata = installationMetadata?.dependencies[name]; + if (!packageMetadata) { + return undefined; + } + + return packageMetadata[0]?.version; +} + +export function getMismatchingVersionsWarnings( + installationMetadata?: InstallationMetadata, + allDependencies?: Record +): string | undefined { + const messages: string[] = []; + try { + const frameworkPackageName = Object.keys(installationMetadata?.dependencies).find( + (packageName) => { + return Object.keys(frameworkPackages).includes(packageName); + } + ); + const cliVersion = getPrimaryVersion('@storybook/cli', installationMetadata); + const frameworkVersion = getPrimaryVersion(frameworkPackageName, installationMetadata); + + if (!cliVersion || !frameworkVersion || semver.eq(cliVersion, frameworkVersion)) { + return undefined; + } + + messages.push( + `${chalk.bold( + 'Attention:' + )} There seems to be a mismatch between your Storybook package versions. This can result in a broken Storybook installation.` + ); + + let versionToCompare: string; + let packageToDisplay: string; + if (semver.lt(cliVersion, frameworkVersion)) { + versionToCompare = frameworkVersion; + packageToDisplay = frameworkPackageName; + } else { + versionToCompare = cliVersion; + packageToDisplay = 'storybook'; + } + + messages.push( + `The version of your storybook core packages should align with ${chalk.yellow( + versionToCompare + )} (from the ${chalk.cyan(packageToDisplay)} package) or higher.` + ); + + const filteredDependencies = Object.entries(installationMetadata?.dependencies).filter( + ([name, packages]) => { + if (Object.keys(storybookCorePackages).includes(name)) { + const packageVersion = packages[0].version; + return packageVersion !== versionToCompare; + } + + return false; + } + ); + + if (filteredDependencies.length > 0) { + messages.push( + `Based on your lockfile, these dependencies should be upgraded:`, + filteredDependencies + .map( + ([name, dep]) => + `${chalk.hex('#ff9800')(name)}: ${dep[0].version} ${ + allDependencies[name] ? '(in your package.json)' : '' + }` + ) + .join('\n') + ); + } + + messages.push( + `You can run ${chalk.cyan( + 'npx storybook@latest upgrade' + )} to upgrade all of your Storybook packages to the latest version. + + Alternatively you can try manually changing the versions to match in your package.json. We also recommend regenerating your lockfile, or running the following command to possibly deduplicate your Storybook package versions: ${chalk.cyan( + installationMetadata.dedupeCommand + )}` + ); + + return messages.join('\n\n'); + } catch (err) { + return undefined; + } +} diff --git a/code/lib/cli/src/automigrate/helpers/hasMultipleVersions.ts b/code/lib/cli/src/doctor/hasMultipleVersions.ts similarity index 100% rename from code/lib/cli/src/automigrate/helpers/hasMultipleVersions.ts rename to code/lib/cli/src/doctor/hasMultipleVersions.ts diff --git a/code/lib/cli/src/doctor/index.ts b/code/lib/cli/src/doctor/index.ts new file mode 100644 index 000000000000..cfafb899f3a4 --- /dev/null +++ b/code/lib/cli/src/doctor/index.ts @@ -0,0 +1,131 @@ +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 { cleanLog } from '../automigrate/helpers/cleanLog'; +import { incompatibleAddons } from '../automigrate/fixes/incompatible-addons'; +import { getDuplicatedDepsWarnings } from './getDuplicatedDepsWarnings'; +import { getIncompatibleAddons } from './getIncompatibleAddons'; +import { getMismatchingVersionsWarnings } from './getMismatchingVersionsWarning'; + +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 doctor] 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 doctor] ❌ Unable to determine Storybook version so the command will not proceed. + 🤔 Are you running storybook doctor from your project directory? Please specify your Storybook config directory with the --config-dir flag. + `); + process.exit(1); + } + + const incompatibleAddonList = await getIncompatibleAddons(mainConfig); + if (incompatibleAddonList.length > 0) { + diagnosticMessages.push(incompatibleAddons.prompt({ incompatibleAddonList })); + } + + const installationMetadata = await packageManager.findInstallations([ + '@storybook/*', + 'storybook', + ]); + + const allDependencies = await packageManager.getAllDependencies(); + const mismatchingVersionMessage = getMismatchingVersionsWarnings( + installationMetadata, + allDependencies + ); + if (mismatchingVersionMessage) { + diagnosticMessages.push(mismatchingVersionMessage); + } else { + diagnosticMessages.push(getDuplicatedDepsWarnings(installationMetadata)?.join('\n')); + } + 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(); +}; diff --git a/code/lib/cli/src/generate.ts b/code/lib/cli/src/generate.ts index e5811888521b..8f37de772933 100644 --- a/code/lib/cli/src/generate.ts +++ b/code/lib/cli/src/generate.ts @@ -23,6 +23,7 @@ import { build } from './build'; import { parseList, getEnvConfig } from './utils'; import versions from './versions'; import { JsPackageManagerFactory } from './js-package-manager'; +import { doctor } from './doctor'; addToGlobalContext('cliVersion', versions.storybook); @@ -170,7 +171,7 @@ command('link ') ); 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 ', 'Force package manager') @@ -189,6 +190,17 @@ command('automigrate [fixId]') }); }); +command('doctor') + .description('Check Storybook for known problems and provide suggestions or fixes') + .option('--package-manager ', 'Force package manager') + .option('-c, --config-dir ', 'Directory of Storybook configuration') + .action(async (options) => { + await doctor(options).catch((e) => { + logger.error(e); + process.exit(1); + }); + }); + command('dev') .option('-p, --port ', 'Port to run Storybook', (str) => parseInt(str, 10)) .option('-h, --host ', 'Host to run Storybook') diff --git a/code/lib/core-events/src/errors/server-errors.ts b/code/lib/core-events/src/errors/server-errors.ts index 964bbbb64a92..f4ecab544773 100644 --- a/code/lib/core-events/src/errors/server-errors.ts +++ b/code/lib/core-events/src/errors/server-errors.ts @@ -406,7 +406,7 @@ export class NoMatchingExportError extends StorybookError { Correct example: { "@storybook/react": "7.5.3", "@storybook/react-vite": "7.5.3", "storybook": "7.5.3" } - Clearing your lock file and reinstalling your dependencies might help as well, as sometimes the version you see in your package.json might not be the one defined in your lock file, leading to version inconsistency issues. + Please run \`npx storybook@latest doctor\` for guidance on how to fix this issue. `; } } diff --git a/code/lib/core-server/src/utils/warnOnIncompatibleAddons.ts b/code/lib/core-server/src/utils/warnOnIncompatibleAddons.ts index 23e17c77bc56..fd4aaac39286 100644 --- a/code/lib/core-server/src/utils/warnOnIncompatibleAddons.ts +++ b/code/lib/core-server/src/utils/warnOnIncompatibleAddons.ts @@ -3,7 +3,7 @@ import { logger } from '@storybook/node-logger'; import chalk from 'chalk'; import dedent from 'ts-dedent'; -import { getIncompatibleAddons } from '../../../cli/src/automigrate/helpers/getIncompatibleAddons'; +import { getIncompatibleAddons } from '../../../cli/src/doctor/getIncompatibleAddons'; export const warnOnIncompatibleAddons = async (config: StorybookConfig) => { const incompatibleAddons = await getIncompatibleAddons(config);