Skip to content

Commit

Permalink
Merge pull request #26035 from storybookjs/norbert/handle-esm-main-error
Browse files Browse the repository at this point in the history
Core: Catch fail to lead `main.ts` over #23972
  • Loading branch information
ndelangen committed Feb 20, 2024
2 parents 1bc793c + 3a87bff commit b0263eb
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 51 deletions.
47 changes: 3 additions & 44 deletions code/lib/cli/src/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { withTelemetry } from '@storybook/core-server';
import {
UpgradeStorybookToLowerVersionError,
UpgradeStorybookToSameVersionError,
UpgradeStorybookUnknownCurrentVersionError,
} from '@storybook/core-events/server-errors';

import chalk from 'chalk';
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<void> {
await withTelemetry('upgrade', { cliOptions: options }, () => doUpgrade(options));
}
44 changes: 42 additions & 2 deletions code/lib/core-common/src/utils/load-main-config.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(',')}}`;
Expand All @@ -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 });
}
}
1 change: 1 addition & 0 deletions code/lib/core-events/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
"ts-dedent": "^2.0.0"
},
"devDependencies": {
"chalk": "^4.1.0",
"typescript": "^5.3.2"
},
"publishConfig": {
Expand Down
108 changes: 108 additions & 0 deletions code/lib/core-events/src/errors/server-errors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { bold, gray, grey, white, yellow, underline } from 'chalk';
import dedent from 'ts-dedent';
import { StorybookError } from './storybook-error';

Expand Down Expand Up @@ -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(<your ESM only module>);`),
''
);

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;

Expand Down Expand Up @@ -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.
`;
}
}
2 changes: 1 addition & 1 deletion code/lib/core-server/src/build-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { oneWayHash, telemetry } 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';
Expand Down
1 change: 1 addition & 0 deletions code/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5612,6 +5612,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
Expand Down

0 comments on commit b0263eb

Please sign in to comment.