diff --git a/code/lib/cli/src/upgrade.ts b/code/lib/cli/src/upgrade.ts index e9f4a8151b1..cb96bd79791 100644 --- a/code/lib/cli/src/upgrade.ts +++ b/code/lib/cli/src/upgrade.ts @@ -6,6 +6,7 @@ import { withTelemetry } from '@storybook/core-server'; import { UpgradeStorybookToLowerVersionError, UpgradeStorybookToSameVersionError, + UpgradeStorybookUnknownCurrentVersionError, } from '@storybook/core-events/server-errors'; import chalk from 'chalk'; @@ -22,7 +23,6 @@ import { } from '@storybook/core-common'; import { automigrate } from './automigrate/index'; import { autoblock } from './autoblock/index'; -import { PreCheckFailure } from './automigrate/types'; type Package = { package: string; @@ -189,26 +189,11 @@ export const doUpgrade = async ({ ); const configDir = userSpecifiedConfigDir || inferredConfigDir || '.storybook'; - let mainConfigLoadingError = ''; - - const mainConfig = await loadMainConfig({ configDir }).catch((err) => { - mainConfigLoadingError = String(err); - return false; - }); + const mainConfig = await loadMainConfig({ configDir }); // GUARDS if (!storybookVersion) { - logger.info(missingStorybookVersionMessage()); - results = { preCheckFailure: PreCheckFailure.UNDETECTED_SB_VERSION }; - } else if ( - typeof mainConfigPath === 'undefined' || - mainConfigLoadingError.includes('No configuration files have been found') - ) { - logger.info(mainjsNotFoundMessage(configDir)); - results = { preCheckFailure: PreCheckFailure.MAINJS_NOT_FOUND }; - } else if (typeof mainConfig === 'boolean') { - logger.info(mainjsExecutionFailureMessage(mainConfigPath, mainConfigLoadingError)); - results = { preCheckFailure: PreCheckFailure.MAINJS_EVALUATION }; + throw new UpgradeStorybookUnknownCurrentVersionError(); } // BLOCKERS @@ -293,32 +278,6 @@ export const doUpgrade = async ({ } }; -function missingStorybookVersionMessage(): string { - return dedent` - [Storybook automigrate] ❌ Unable to determine Storybook version so that the automigrations will be skipped. - 🤔 Are you running automigrate from your project directory? Please specify your Storybook config directory with the --config-dir flag. - `; -} - -function mainjsExecutionFailureMessage( - mainConfigPath: string, - mainConfigLoadingError: string -): string { - return dedent` - [Storybook automigrate] ❌ Failed trying to evaluate ${chalk.blue( - mainConfigPath - )} with the following error: ${mainConfigLoadingError} - - Please fix the error and try again. - `; -} - -function mainjsNotFoundMessage(configDir: string): string { - return dedent`[Storybook automigrate] Could not find or evaluate your Storybook main.js config directory at ${chalk.blue( - configDir - )} so the automigrations will be skipped. 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.`; -} - export async function upgrade(options: UpgradeOptions): Promise { await withTelemetry('upgrade', { cliOptions: options }, () => doUpgrade(options)); } diff --git a/code/lib/core-common/src/utils/load-main-config.ts b/code/lib/core-common/src/utils/load-main-config.ts index 492ede4f669..b5001418597 100644 --- a/code/lib/core-common/src/utils/load-main-config.ts +++ b/code/lib/core-common/src/utils/load-main-config.ts @@ -1,7 +1,12 @@ -import path from 'path'; +import path, { relative } from 'path'; import type { StorybookConfig } from '@storybook/types'; import { serverRequire, serverResolve } from './interpret-require'; import { validateConfigurationFiles } from './validate-configuration-files'; +import { readFile } from 'fs/promises'; +import { + MainFileESMOnlyImportError, + MainFileEvaluationError, +} from '@storybook/core-events/server-errors'; export async function loadMainConfig({ configDir = '.storybook', @@ -18,5 +23,40 @@ export async function loadMainConfig({ delete require.cache[mainJsPath]; } - return serverRequire(mainJsPath); + try { + const out = await serverRequire(mainJsPath); + return out; + } catch (e) { + if (!(e instanceof Error)) { + throw e; + } + if (e.message.match(/Cannot use import statement outside a module/)) { + const location = relative(process.cwd(), mainJsPath); + const numFromStack = e.stack?.match(new RegExp(`${location}:(\\d+):(\\d+)`))?.[1]; + let num; + let line; + + if (numFromStack) { + const contents = await readFile(mainJsPath, 'utf-8'); + const lines = contents.split('\n'); + num = parseInt(numFromStack, 10) - 1; + line = lines[num]; + } + + const out = new MainFileESMOnlyImportError({ + line, + location, + num, + }); + + delete out.stack; + + throw out; + } + + throw new MainFileEvaluationError({ + location: relative(process.cwd(), mainJsPath), + error: e, + }); + } } diff --git a/code/lib/core-common/src/utils/validate-configuration-files.ts b/code/lib/core-common/src/utils/validate-configuration-files.ts index 57e1cffeab7..0f7c1ecaaeb 100644 --- a/code/lib/core-common/src/utils/validate-configuration-files.ts +++ b/code/lib/core-common/src/utils/validate-configuration-files.ts @@ -5,6 +5,7 @@ import slash from 'slash'; import { once } from '@storybook/node-logger'; import { boost } from './interpret-files'; +import { MainFileMissingError } from '@storybook/core-events/server-errors'; export async function validateConfigurationFiles(configDir: string) { const extensionsPattern = `{${Array.from(boost).join(',')}}`; @@ -20,9 +21,6 @@ export async function validateConfigurationFiles(configDir: string) { } if (!mainConfigPath) { - throw new Error(dedent` - No configuration files have been found in your configDir (${path.resolve(configDir)}). - Storybook needs "main.js" file, please add it (or pass a custom config dir flag to Storybook to tell where your main.js file is located at). - `); + throw new MainFileMissingError({ location: configDir }); } } diff --git a/code/lib/core-events/package.json b/code/lib/core-events/package.json index e582c124ddd..ce38d804143 100644 --- a/code/lib/core-events/package.json +++ b/code/lib/core-events/package.json @@ -81,6 +81,7 @@ "ts-dedent": "^2.0.0" }, "devDependencies": { + "chalk": "^4.1.0", "typescript": "^5.3.2" }, "publishConfig": { diff --git a/code/lib/core-events/src/errors/server-errors.ts b/code/lib/core-events/src/errors/server-errors.ts index 2b522ad291f..a9a9e758d70 100644 --- a/code/lib/core-events/src/errors/server-errors.ts +++ b/code/lib/core-events/src/errors/server-errors.ts @@ -1,3 +1,4 @@ +import { bold, gray, grey, white, yellow, underline } from 'chalk'; import dedent from 'ts-dedent'; import { StorybookError } from './storybook-error'; @@ -394,6 +395,98 @@ export class NoMatchingExportError extends StorybookError { } } +export class MainFileESMOnlyImportError extends StorybookError { + readonly category = Category.CORE_SERVER; + + readonly code = 5; + + public documentation = + 'https://github.com/storybookjs/storybook/issues/23972#issuecomment-1948534058'; + + constructor( + public data: { location: string; line: string | undefined; num: number | undefined } + ) { + super(); + } + + template() { + const message = [ + `Storybook failed to load ${this.data.location}`, + '', + `It looks like the file tried to load/import an ESM only module.`, + `Support for this is currently limited in ${this.data.location}`, + `You can import ESM modules in your main file, but only as dynamic import.`, + '', + ]; + if (this.data.line) { + message.push( + white( + `In your ${yellow(this.data.location)} file, line ${bold.cyan( + this.data.num + )} threw an error:` + ), + grey(this.data.line) + ); + } + + message.push( + '', + white(`Convert the static import to a dynamic import ${underline('where they are used')}.`), + white(`Example:`) + ' ' + gray(`await import();`), + '' + ); + + return message.join('\n'); + } +} + +export class MainFileMissingError extends StorybookError { + readonly category = Category.CORE_SERVER; + + readonly code = 6; + + readonly stack = ''; + + public readonly documentation = 'https://storybook.js.org/docs/configure'; + + constructor(public data: { location: string }) { + super(); + } + + template() { + return dedent` + No configuration files have been found in your configDir: ${yellow(this.data.location)}. + Storybook needs a "main.js" file, please add it. + + You can pass a --config-dir flag to tell Storybook, where your main.js file is located at). + `; + } +} + +export class MainFileEvaluationError extends StorybookError { + readonly category = Category.CORE_SERVER; + + readonly code = 7; + + readonly stack = ''; + + constructor(public data: { location: string; error: Error }) { + super(); + } + + template() { + const errorText = white( + (this.data.error.stack || this.data.error.message).replaceAll(process.cwd(), '') + ); + + return dedent` + Storybook couldn't evaluate your ${yellow(this.data.location)} file. + + ${errorText} + `; + } +} + export class GenerateNewProjectOnInitError extends StorybookError { readonly category = Category.CLI_INIT; @@ -468,3 +561,18 @@ export class UpgradeStorybookToSameVersionError extends StorybookError { `; } } + +export class UpgradeStorybookUnknownCurrentVersionError extends StorybookError { + readonly category = Category.CLI_UPGRADE; + + readonly code = 5; + + template() { + return dedent` + We couldn't determine the current version of Storybook in your project. + + Are you running the Storybook CLI in a project without Storybook? + It might help if you specify your Storybook config directory with the --config-dir flag. + `; + } +} diff --git a/code/lib/core-server/src/build-dev.ts b/code/lib/core-server/src/build-dev.ts index 67990ff2d43..4cb0de5efca 100644 --- a/code/lib/core-server/src/build-dev.ts +++ b/code/lib/core-server/src/build-dev.ts @@ -15,7 +15,7 @@ import { telemetry, oneWayHash } from '@storybook/telemetry'; import { join, relative, resolve } from 'path'; import { deprecate } from '@storybook/node-logger'; -import dedent from 'ts-dedent'; +import { dedent } from 'ts-dedent'; import { readFile } from 'fs-extra'; import { MissingBuilderError } from '@storybook/core-events/server-errors'; import { storybookDevServer } from './dev-server'; diff --git a/code/yarn.lock b/code/yarn.lock index 755a51216c1..11a4a61e2df 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -5528,6 +5528,7 @@ __metadata: version: 0.0.0-use.local resolution: "@storybook/core-events@workspace:lib/core-events" dependencies: + chalk: "npm:^4.1.0" ts-dedent: "npm:^2.0.0" typescript: "npm:^5.3.2" languageName: unknown