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

ESM: Presets loading async/ESM #28802

Closed
wants to merge 13 commits into from
25 changes: 14 additions & 11 deletions code/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,18 +131,21 @@ const config: StorybookConfig = {
viteFinal: (viteConfig, { configType }) =>
mergeConfig(viteConfig, {
resolve: {
alias: {
...(configType === 'DEVELOPMENT'
? {
'@storybook/components': componentsPath,
'storybook/internal/components': componentsPath,
'@storybook/manager-api': managerApiPath,
'storybook/internal/manager-api': managerApiPath,
}
: {}),
},
// alias: {
// ...(configType === 'DEVELOPMENT'
// ? {
// '@storybook/components': componentsPath,
// 'storybook/internal/components': componentsPath,
// '@storybook/manager-api': managerApiPath,
// 'storybook/internal/manager-api': managerApiPath,
// }
// : {}),
// },
},
optimizeDeps: {
force: true,
exclude: ['firebase-functions'],
},
optimizeDeps: { force: true },
build: {
// disable sourcemaps in CI to not run out of memory
sourcemap: process.env.CI !== 'true',
Expand Down
5 changes: 2 additions & 3 deletions code/addons/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
},
"./preset": {
"types": "./dist/preset.d.ts",
"import": "./dist/preset.js",
"require": "./dist/preset.js"
},
"./blocks": {
Expand Down Expand Up @@ -107,15 +106,15 @@
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
"rehype-external-links": "^3.0.0",
"rehype-slug": "^6.0.0",
"ts-dedent": "^2.0.0"
},
"devDependencies": {
"@mdx-js/mdx": "^3.0.0",
"@rollup/pluginutils": "^5.0.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rehype-external-links": "^3.0.0",
"rehype-slug": "^6.0.0",
"typescript": "^5.3.2",
"vite": "^4.0.4"
},
Expand Down
6 changes: 3 additions & 3 deletions code/addons/essentials/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ interface PresetOptions {
viewport?: boolean;
}

const requireMain = (configDir: string) => {
const requireMain = async (configDir: string) => {
const absoluteConfigDir = isAbsolute(configDir) ? configDir : join(process.cwd(), configDir);
const mainFile = join(absoluteConfigDir, 'main');

return serverRequire(mainFile) ?? {};
};

export function addons(options: PresetOptions) {
export async function addons(options: PresetOptions) {
const checkInstalled = (addonName: string, main: any) => {
const addon = `@storybook/addon-${addonName}`;
const existingAddon = main.addons?.find((entry: string | { name: string }) => {
Expand All @@ -76,7 +76,7 @@ export function addons(options: PresetOptions) {
return !!existingAddon;
};

const main = requireMain(options.configDir);
const main = await requireMain(options.configDir);

// NOTE: The order of these addons is important.
return [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export async function generateModernIframeScriptCode(options: Options, projectRo
const { presets, configDir } = options;
const frameworkName = await getFrameworkName(options);

const previewOrConfigFile = loadPreviewOrConfigFile({ configDir });
const previewOrConfigFile = await loadPreviewOrConfigFile({ configDir });
const previewAnnotations = await presets.apply<PreviewAnnotation[]>(
'previewAnnotations',
[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export async function webpack(config: Configuration, options: Options) {

// Check whether user has a custom webpack config file and
// return the (extended) base configuration if it's not available.
const customConfig = loadCustomWebpackConfig(configDir);
const customConfig = await loadCustomWebpackConfig(configDir);

if (typeof customConfig === 'function') {
logger.info('=> Loading custom Webpack config (full-control mode).');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const getVirtualModules = async (options: Options) => {
return slash(entry);
}
),
loadPreviewOrConfigFile(options),
await loadPreviewOrConfigFile(options),
].filter(Boolean);

const storiesFilename = 'storybook-stories.js';
Expand Down
2 changes: 1 addition & 1 deletion code/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,11 +273,11 @@
"@types/express": "^4.17.21",
"browser-assert": "^1.2.1",
"esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0",
"esbuild-register": "^3.5.0",
"express": "^4.19.2",
"process": "^0.11.10",
"recast": "^0.23.5",
"semver": "^7.6.2",
"ts-import": "^5.0.0-beta.0",
"ws": "^8.2.3"
},
"devDependencies": {
Expand Down
32 changes: 16 additions & 16 deletions code/core/src/common/presets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@

const result = await presets.apply('foo', []);

expect(result).toEqual(['foo', 'dracarys', 'valar morghulis', 'bar']);

Check failure on line 141 in code/core/src/common/presets.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/common/presets.test.ts > presets > loads and applies presets when they are combined in another preset

AssertionError: expected [] to deeply equal [ 'foo', 'dracarys', …(2) ] - Expected + Received - Array [ - "foo", - "dracarys", - "valar morghulis", - "bar", - ] + Array [] ❯ src/common/presets.test.ts:141:20
});

it('loads and applies presets when they are declared as a string', async () => {
Expand All @@ -162,7 +162,7 @@

await expect(testPresets()).resolves.toBeUndefined();

expect(mockPresetFooExtendWebpack).toHaveBeenCalled();

Check failure on line 165 in code/core/src/common/presets.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/common/presets.test.ts > presets > loads and applies presets when they are declared as a string

AssertionError: expected "spy" to be called at least once ❯ src/common/presets.test.ts:165:40
expect(mockPresetBarExtendBabel).toHaveBeenCalled();
});

Expand All @@ -189,7 +189,7 @@

await expect(testPresets()).resolves.toBeUndefined();

expect(mockPresetFooExtendWebpack).toHaveBeenCalled();

Check failure on line 192 in code/core/src/common/presets.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/common/presets.test.ts > presets > loads and applies presets when they are declared as an object without props

AssertionError: expected "spy" to be called at least once ❯ src/common/presets.test.ts:192:40
expect(mockPresetBarExtendBabel).toHaveBeenCalled();
});

Expand Down Expand Up @@ -222,7 +222,7 @@

await expect(testPresets()).resolves.toBeUndefined();

expect(mockPresetFooExtendWebpack).toHaveBeenCalledWith(expect.anything(), {

Check failure on line 225 in code/core/src/common/presets.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/common/presets.test.ts > presets > loads and applies presets when they are declared as an object with props

AssertionError: expected "spy" to be called with arguments: [ Anything, { foo: 1, …(2) } ] Received: Number of calls: 0 ❯ src/common/presets.test.ts:225:40
foo: 1,
presetsList: expect.anything(),
presets: expect.anything(),
Expand Down Expand Up @@ -268,7 +268,7 @@

await expect(testPresets()).resolves.toBeUndefined();

expect(mockPresetFooExtendWebpack).toHaveBeenCalled();

Check failure on line 271 in code/core/src/common/presets.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/common/presets.test.ts > presets > loads and applies presets when they are declared as a string and as an object

AssertionError: expected "spy" to be called at least once ❯ src/common/presets.test.ts:271:40
expect(mockPresetBarExtendBabel).toHaveBeenCalledWith(expect.anything(), {
bar: 'a',
presetsList: expect.arrayContaining([
Expand Down Expand Up @@ -318,7 +318,7 @@

await expect(testPresets()).resolves.toBeUndefined();

expect(mockPresetFooExtendWebpack).toHaveBeenCalled();

Check failure on line 321 in code/core/src/common/presets.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/common/presets.test.ts > presets > applies presets in chain

AssertionError: expected "spy" to be called at least once ❯ src/common/presets.test.ts:321:40
expect(mockPresetBarExtendWebpack).toHaveBeenCalledWith(expect.anything(), {
bar: 'a',
presetsList: expect.arrayContaining([
Expand All @@ -345,7 +345,7 @@

const output = await presets.apply('bar');

expect(mockPresetBar).toHaveBeenCalledWith(undefined, expect.any(Object));

Check failure on line 348 in code/core/src/common/presets.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/common/presets.test.ts > presets > allows for presets to export presets array

AssertionError: expected "spy" to be called with arguments: [ undefined, Any<Object> ] Received: Number of calls: 0 ❯ src/common/presets.test.ts:348:27

expect(input).toBe(output);
});
Expand All @@ -372,7 +372,7 @@

const output = await presets.apply('bar');

expect(mockPresetFoo).toHaveBeenCalledWith({ ...storybookOptions, ...presetOptions });

Check failure on line 375 in code/core/src/common/presets.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/common/presets.test.ts > presets > allows for presets to export presets fn

AssertionError: expected "spy" to be called with arguments: [ { a: 1, b: 2 } ] Received: Number of calls: 0 ❯ src/common/presets.test.ts:375:27
expect(mockPresetBar).toHaveBeenCalledWith(undefined, expect.any(Object));

expect(input).toBe(output);
Expand All @@ -384,66 +384,66 @@
});
});
describe('resolveAddonName', () => {
it('should resolve packages with metadata (relative path)', () => {
it('should resolve packages with metadata (relative path)', async () => {
mockPreset('./local/preset', {
presets: [],
});
expect(resolveAddonName({} as any, './local/preset', {})).toEqual({
expect(await resolveAddonName({} as any, './local/preset', {})).toEqual({
name: './local/preset',
type: 'presets',
});
});

it('should resolve packages with metadata (absolute path)', () => {
it('should resolve packages with metadata (absolute path)', async () => {
mockPreset('/absolute/preset', {
presets: [],
});
expect(resolveAddonName({} as any, '/absolute/preset', {})).toEqual({
expect(await resolveAddonName({} as any, '/absolute/preset', {})).toEqual({
name: '/absolute/preset',
type: 'presets',
});
});

it('should resolve packages without metadata', () => {
expect(resolveAddonName({} as any, '@storybook/preset-create-react-app', {})).toEqual({
it('should resolve packages without metadata', async () => {
expect(await resolveAddonName({} as any, '@storybook/preset-create-react-app', {})).toEqual({
name: '@storybook/preset-create-react-app',
type: 'presets',
});
});

it('should resolve managerEntries', () => {
expect(resolveAddonName({} as any, '@storybook/addon-actions/register.js', {})).toEqual({
it('should resolve managerEntries', async () => {
expect(await resolveAddonName({} as any, '@storybook/addon-actions/register.js', {})).toEqual({
name: '@storybook/addon-actions/register.js',
managerEntries: [normalize('@storybook/addon-actions/register')],
type: 'virtual',
});
});

it('should resolve managerEntries from new /manager path', () => {
expect(resolveAddonName({} as any, '@storybook/addon-actions/manager', {})).toEqual({
it('should resolve managerEntries from new /manager path', async () => {
expect(await resolveAddonName({} as any, '@storybook/addon-actions/manager', {})).toEqual({
name: '@storybook/addon-actions/manager',
managerEntries: [normalize('@storybook/addon-actions/manager')],
type: 'virtual',
});
});

it('should resolve presets', () => {
expect(resolveAddonName({} as any, '@storybook/addon-docs/preset', {})).toEqual({
it('should resolve presets', async () => {
expect(await resolveAddonName({} as any, '@storybook/addon-docs/preset', {})).toEqual({
name: '@storybook/addon-docs/preset',
type: 'presets',
});
});

it('should resolve preset packages', () => {
expect(resolveAddonName({} as any, '@storybook/addon-essentials', {})).toEqual({
it('should resolve preset packages', async () => {
expect(await resolveAddonName({} as any, '@storybook/addon-essentials', {})).toEqual({
name: '@storybook/addon-essentials',
type: 'presets',
});
});

it('should error on invalid inputs', () => {
it('should error on invalid inputs', async () => {
// @ts-expect-error (invalid use)
expect(() => resolveAddonName({} as any, null, {})).toThrow();
await expect(async () => resolveAddonName({} as any, null, {})).rejects.toThrow();
});
});

Expand Down Expand Up @@ -482,7 +482,7 @@
0,
{}
);
expect(loaded).toMatchInlineSnapshot(`

Check failure on line 485 in code/core/src/common/presets.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/common/presets.test.ts > loadPreset > should prepend framework field to list of presets

Error: Snapshot `loadPreset > should prepend framework field to list of presets 1` mismatched - Expected + Received @@ -1,17 +1,7 @@ [ { - "name": "@storybook/preset-typescript", - "options": {}, - "preset": {}, - }, - { - "name": "@storybook/addon-docs/preset", - "options": {}, - "preset": {}, - }, - { "name": { "addons": [ "@storybook/addon-docs/preset", ], "framework": "@storybook/react", ❯ src/common/presets.test.ts:485:20
[
{
"name": "@storybook/preset-typescript",
Expand Down Expand Up @@ -534,7 +534,7 @@
0,
{}
);
expect(loaded).toEqual([

Check failure on line 537 in code/core/src/common/presets.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/common/presets.test.ts > loadPreset > should resolve all addons & presets in correct order

AssertionError: expected [ { …(3) }, { …(3) }, { …(3) }, …(2) ] to deeply equal [ { …(3) }, { …(3) }, { …(3) }, …(7) ] - Expected + Received Array [ Object { - "name": "@storybook/preset-typescript", - "options": Object {}, - "preset": Object {}, - }, - Object { - "name": "@storybook/addon-docs/preset", - "options": Object {}, - "preset": Object {}, - }, - Object { "name": "@storybook/addon-actions/register.js", "options": Object {}, "preset": Object { "managerEntries": Array [ "@storybook\\addon-actions\\register", ], }, }, Object { "name": "addon-foo/register.js", "options": Object {}, "preset": Object { "managerEntries": Array [ "addon-foo\\register", ], }, - }, - Object { - "name": "@storybook/addon-interactions/preset", - "options": Object {}, - "preset": Object {}, - }, - Object { - "name": "@storybook/addon-cool", - "options": Object {}, - "preset": Object {}, - }, - Object { - "name": "addon-bar", - "options": Object {}, - "preset": Object {}, }, Object { "name": "addon-baz/register.js", "options": Object {}, "preset": Object { "managerEntries": Array [ "addon-baz\\register", ], }, }, Object { "name": "@storybook/addon-notes/register-panel", "options": Object {}, "preset": Object { "managerEntries": Array [ "@storybook\\addon-notes\\register-panel", ], }, }, Object { "name": Object { "addons": Array [ "@storybook/addon-docs/preset", "@storybook/addon-actions/register.js", "addon-foo/register.js", "addon-bar", "addon-baz/register.js", "@storybook/addon-notes/register-panel", ], "name": "", "presets": Array [ "@storybook/preset-typescript", ], "type": "virtual", }, "options": Object {}, "preset": Object {}, }, ] ❯ src/common/presets.test.ts:537:20
{
name: '@storybook/preset-typescript',
options: {},
Expand Down
96 changes: 72 additions & 24 deletions code/core/src/common/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
import { logger } from '@storybook/core/node-logger';
import { CriticalPresetLoadError } from '@storybook/core/server-errors';

import * as resolveHelper from 'resolve.exports';
import { dedent } from 'ts-dedent';

import { interopRequireDefault } from './utils/interpret-require';
Expand Down Expand Up @@ -49,19 +50,26 @@ function resolvePathToMjs(filePath: string): string {
return filePath;
}

function resolvePresetFunction<T = any>(
async function resolvePresetFunction<T = any>(
input: T[] | Function,
presetOptions: any,
storybookOptions: InterPresetOptions
): T[] {
if (isFunction(input)) {
return [...input({ ...storybookOptions, ...presetOptions })];
}
if (Array.isArray(input)) {
return [...input];
}
): Promise<T[]> {
try {
if (isFunction(input)) {
const result = await input({ ...storybookOptions, ...presetOptions });
console.log({ result });
return [...result];
}
if (Array.isArray(input)) {
return [...input];
}

return [];
return [];
} catch (e) {
console.log({ e, input });
throw e;
}
}

/**
Expand All @@ -81,11 +89,11 @@ function resolvePresetFunction<T = any>(
* => { type: 'presets', item: { name: '@storybook/addon-docs/preset', options } }
*/

export const resolveAddonName = (
export const resolveAddonName = async (
configDir: string,
name: string,
options: any
): CoreCommon_ResolvedAddonPreset | CoreCommon_ResolvedAddonVirtual | undefined => {
): Promise<CoreCommon_ResolvedAddonPreset | CoreCommon_ResolvedAddonVirtual | undefined> => {
const resolve = name.startsWith('/') ? safeResolve : safeResolveFrom.bind(null, configDir);
const resolved = resolve(name);

Expand Down Expand Up @@ -122,7 +130,29 @@ export const resolveAddonName = (
// Vite will be broken in such cases, because it does not process absolute paths,
// and it will try to import from the bare import, breaking in pnp/pnpm.
const absolutizeExport = (exportName: string, preferMJS: boolean) => {
const found = resolve(`${name}${exportName}`);
let combo = `${name}${exportName}`;
try {
const pkg = require(join(name, 'package.json'));
let subpath = resolveHelper.exports(pkg, exportName);

if (!subpath || subpath.length < 1) {
subpath = resolveHelper.exports(pkg, '.' + exportName);
}
if (!subpath || subpath.length < 1) {
subpath = resolveHelper.exports(pkg, '.' + exportName.replace(/^\//, ''));
}
if (subpath) {
combo = join(name, ...subpath);
}

if (!subpath || subpath.length < 1) {
combo = require.resolve(combo);
}
} catch (err) {
// failed = true;
}

const found = resolve(combo);

if (found) {
return preferMJS ? resolvePathToMjs(found) : found;
Expand Down Expand Up @@ -190,14 +220,14 @@ export const resolveAddonName = (

const map =
({ configDir }: InterPresetOptions) =>
(item: any) => {
async (item: any) => {
const options = isObject(item) ? item['options'] || undefined : undefined;
const name = isObject(item) ? item['name'] : item;

let resolved;

try {
resolved = resolveAddonName(configDir, name, options);
resolved = await resolveAddonName(configDir, name, options);
} catch (err) {
logger.error(
`Addon value should end in /manager or /preview or /register OR it should be a valid preset https://storybook.js.org/docs/react/addons/writing-presets/\n${item}`
Expand Down Expand Up @@ -269,19 +299,37 @@ export async function loadPreset(
};
}

const subPresets = resolvePresetFunction(
presetsInput,
presetOptions,
storybookOptions
).filter(filter);
const subAddons = resolvePresetFunction(addonsInput, presetOptions, storybookOptions).filter(
filter
);
if (presetsInput && presetsInput.length > 1) {
console.log({ presetsInput });
}
if (addonsInput && addonsInput.length > 1) {
console.log({ addonsInput });
}

let subPresets: PresetConfig[] = [];
let subAddons: PresetConfig[] = [];

try {
subPresets = (
await resolvePresetFunction(presetsInput, presetOptions, storybookOptions)
).filter(filter);
} catch (a) {
console.log({ a });
}
try {
subAddons = (
await resolvePresetFunction(addonsInput, presetOptions, storybookOptions)
).filter(filter);
} catch (e) {
console.log({ e });
}

return [
...(await loadPresets([...subPresets], level + 1, storybookOptions)),
...(await loadPresets(
[...subAddons.map(map(storybookOptions))].filter(Boolean) as PresetConfig[],
(await Promise.all([...subAddons.map(map(storybookOptions))])).filter(
Boolean
) as PresetConfig[],
level + 1,
storybookOptions
)),
Expand Down Expand Up @@ -415,7 +463,7 @@ export async function loadAllPresets(

const presetsConfig: PresetConfig[] = [
...corePresets,
...loadCustomPresets(options),
...(await loadCustomPresets(options)),
...overridePresets,
];

Expand Down
Loading