Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: Fix fail to load main.ts error message #26035

Merged
merged 11 commits into from
Feb 20, 2024
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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could this bring potential problems for the core-events package? as chalk would be prebundled in something that goes to the browser as well

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How, could it get pre-bundled into something that goes into the browser, due to this change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK this doesn't have an affect on any code we ship to the browser, as long as that browser code doesn't reference chalk.

I had to move chalk into here as @vanessayuenn asked me to move this to core-events: #26035 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also have a funny feeling about adding chalk to that package, but I'll leave that decision to you

"typescript": "^5.3.2"
},
"publishConfig": {
Expand Down
109 changes: 109 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,99 @@ 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}..`,
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
'',
`It looks like the file tried to load/import an ESM only module.`,
`Support for this is currently limited in ${this.data.location}.`,
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
`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, which looks like this:`
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
),
grey(this.data.line)
);
}

message.push(
'',
white(`Convert the static import to an dynamic import ${underline('where they are used')}.`),
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
white(`Example:`) + ' ' + gray(`await import(<your ESM only module>);`),
'',
'For more information, please read the documentation link below.'
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
);

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 "main.js" file, please add it.
ndelangen marked this conversation as resolved.
Show resolved Hide resolved

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 +562,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?
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
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 { 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';
Expand Down
1 change: 1 addition & 0 deletions code/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down