From 8845f5f869f8ff0888c277dcd9ab48bd9611d8ac Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 23 Jul 2025 10:14:37 +0200 Subject: [PATCH 1/9] remove unnecessary postbuild scripts --- code/core/build-config.ts | 3 --- code/lib/cli-storybook/build-config.ts | 4 ---- code/lib/create-storybook/build-config.ts | 4 ---- 3 files changed, 11 deletions(-) diff --git a/code/core/build-config.ts b/code/core/build-config.ts index 6026c4c276c9..76c21ac6b105 100644 --- a/code/core/build-config.ts +++ b/code/core/build-config.ts @@ -20,9 +20,6 @@ const config: BuildEntries = { throwOnError: true, }); }, - postbuild: async (cwd) => { - await fs.chmod(path.join(cwd, 'dist', 'bin', 'dispatcher.js'), 0o755); - }, entries: { node: [ { diff --git a/code/lib/cli-storybook/build-config.ts b/code/lib/cli-storybook/build-config.ts index f597354439e4..403def7e5b32 100644 --- a/code/lib/cli-storybook/build-config.ts +++ b/code/lib/cli-storybook/build-config.ts @@ -11,10 +11,6 @@ const config: BuildEntries = { }, ], }, - postbuild: async (cwd) => { - const { chmod } = await import('node:fs/promises'); - await chmod(join(cwd, 'dist', 'bin', 'index.js'), 0o755); - }, }; export default config; diff --git a/code/lib/create-storybook/build-config.ts b/code/lib/create-storybook/build-config.ts index 6b84dacc39c6..ffd06463e796 100644 --- a/code/lib/create-storybook/build-config.ts +++ b/code/lib/create-storybook/build-config.ts @@ -16,10 +16,6 @@ const config: BuildEntries = { }, ], }, - postbuild: async (cwd) => { - const { chmod } = await import('node:fs/promises'); - await chmod(join(cwd, 'dist', 'bin', 'index.js'), 0o755); - }, }; export default config; From 2c4c53eb529b1430ebec00f7cad6ab27b6243eec Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 23 Jul 2025 10:15:40 +0200 Subject: [PATCH 2/9] fix running postbuild scripts, refactor generate bundle to support metafile generation in watch mode --- scripts/build/utils/entry-utils.ts | 6 +- scripts/build/utils/generate-bundle.ts | 258 +++++++++++++------------ 2 files changed, 141 insertions(+), 123 deletions(-) diff --git a/scripts/build/utils/entry-utils.ts b/scripts/build/utils/entry-utils.ts index 33bd87ffcf3b..c1685c67ed89 100644 --- a/scripts/build/utils/entry-utils.ts +++ b/scripts/build/utils/entry-utils.ts @@ -3,14 +3,14 @@ import { join } from 'node:path'; import * as esbuild from 'esbuild'; +export type EntryType = 'node' | 'browser' | 'runtime' | 'globalizedRuntime'; + export type BuildEntry = { exportEntries?: ('.' | `./${string}`)[]; // the keys in the package.json's export map, e.g. ["./internal/manager-api", "./manager-api"] entryPoint: `./src/${string}`; // the source file to bundle, e.g. "./src/manager-api/index.ts" dts?: false; // default to generating d.ts files for all entries, except if set to false }; -export type BuildEntriesByPlatform = Partial< - Record<'node' | 'browser' | 'runtime' | 'globalizedRuntime', BuildEntry[]> ->; +export type BuildEntriesByPlatform = Partial>; export type EsbuildContextOptions = Parameters<(typeof esbuild)['context']>[0]; diff --git a/scripts/build/utils/generate-bundle.ts b/scripts/build/utils/generate-bundle.ts index 585d4afb125b..cc4ee0823adc 100644 --- a/scripts/build/utils/generate-bundle.ts +++ b/scripts/build/utils/generate-bundle.ts @@ -15,7 +15,12 @@ import { SUPPORTED_FEATURES, } from '../../../code/core/src/shared/constants/environments-support'; import { resolvePackageDir } from '../../../code/core/src/shared/utils/module'; -import { type BuildEntries, type EsbuildContextOptions, getExternal } from './entry-utils'; +import { + type BuildEntries, + type EntryType, + type EsbuildContextOptions, + getExternal, +} from './entry-utils'; // repo root/bench/esbuild-metafiles/core const DIR_METAFILE_BASE = join( @@ -29,6 +34,25 @@ const DIR_METAFILE_BASE = join( ); export const DIR_CODE = join(import.meta.dirname, '..', '..', '..', 'code'); +function metafileWriterPlugin(entryType: EntryType, outputDir: string): esbuild.Plugin { + return { + name: 'metafile-writer', + setup(build) { + build.onEnd(async (result) => { + if (result.errors.length || !result.metafile) { + return; + } + const outputFile = join(outputDir, `${entryType}.json`); + if (existsSync(outputFile)) { + await rm(outputFile, { force: true }); + } + await mkdir(outputDir, { recursive: true }); + await writeFile(outputFile, JSON.stringify(result.metafile, null, 2)); + }); + }, + }; +} + export async function generateBundle({ cwd, entry, @@ -44,70 +68,55 @@ export async function generateBundle({ }) { const DIR_CWD = cwd; const DIR_REL = relative(DIR_CODE, DIR_CWD); - const DIR_METAFILE = join(DIR_METAFILE_BASE, name); const external = (await getExternal(DIR_CWD)).runtimeExternal; const { entries, postbuild } = entry; - function defineESBuildContext(id: string, ...input: Parameters) { - const sharedOptions = { - format: 'esm', - bundle: true, - legalComments: 'none', - ignoreAnnotations: true, - splitting: true, - metafile: true, - minifyIdentifiers: true, - minifySyntax: isProduction, - minifyWhitespace: false, - keepNames: true, // required to show correct error messages based on class names - outbase: 'src', - outdir: 'dist', - treeShaking: true, - color: true, - external, - define: { - /* - * We need to disable the default behavior of replacing process.env.NODE_ENV with "development" - * Because we have code that reads this value to determine if the code is running in a production environment. - * @see 6th bullet in "browser" section in https://esbuild.github.io/api/#platform - */ - 'process.env.NODE_ENV': 'process.env.NODE_ENV', - }, - } as const satisfies EsbuildContextOptions; - - const [config, ...rest] = input; - const cloned = { ...config }; - - if (postbuild) { - cloned.plugins = [ - ...(cloned.plugins ?? []), - { - name: 'postbuild', - setup(build) { - build.onEnd(async (result) => { - if (result.errors.length) { - return; - } - await postbuild(DIR_CWD); - }); - }, - }, - ]; - } - - return [ - id, - esbuild.context( - { - ...sharedOptions, - ...config, + const sharedOptions = { + format: 'esm', + bundle: true, + legalComments: 'none', + ignoreAnnotations: true, + splitting: true, + metafile: true, + minifyIdentifiers: true, + minifySyntax: isProduction, + minifyWhitespace: false, + keepNames: true, // required to show correct error messages based on class names + outbase: 'src', + outdir: 'dist', + treeShaking: true, + color: true, + external, + define: { + /* + * We need to disable the default behavior of replacing process.env.NODE_ENV with "development" + * Because we have code that reads this value to determine if the code is running in a production environment. + * @see 6th bullet in "browser" section in https://esbuild.github.io/api/#platform + */ + 'process.env.NODE_ENV': 'process.env.NODE_ENV', + }, + plugins: [ + { + name: 'postbuild', + setup(build) { + build.onEnd(async (result) => { + if (!postbuild) { + return; + } + if (result.errors.length) { + console.log('Errors found, skipping postbuild'); + return; + } + console.log('Running postbuild script'); + await postbuild(DIR_CWD); + }); }, - ...rest - ), - ] as const; - } + }, + ], + } as const satisfies EsbuildContextOptions; const runtimeOptions = { + ...sharedOptions, platform: 'browser', target: BROWSER_TARGETS, supported: SUPPORTED_FEATURES, @@ -139,87 +148,96 @@ export async function generateBundle({ }, } as const satisfies EsbuildContextOptions; - const contexts = [ - entries.node && - defineESBuildContext('node', { + const contexts: Array> = []; + + if (entries.node) { + contexts.push( + esbuild.context({ + ...sharedOptions, entryPoints: entries.node.map(({ entryPoint }) => entryPoint), platform: 'node', target: NODE_TARGET, chunkNames: '_node-chunks/[name]-[hash]', banner: { js: dedent` - import CJS_COMPAT_NODE_URL from 'node:url'; - import CJS_COMPAT_NODE_PATH from 'node:path'; - import CJS_COMPAT_NODE_MODULE from "node:module"; - - const __filename = CJS_COMPAT_NODE_URL.fileURLToPath(import.meta.url); - const __dirname = CJS_COMPAT_NODE_PATH.dirname(__filename); - const require = CJS_COMPAT_NODE_MODULE.createRequire(import.meta.url); - // ------------------------------------------------------------ - // end of CJS compatibility banner, injected by Storybook's esbuild configuration - // ------------------------------------------------------------ - `, + import CJS_COMPAT_NODE_URL from 'node:url'; + import CJS_COMPAT_NODE_PATH from 'node:path'; + import CJS_COMPAT_NODE_MODULE from "node:module"; + + const __filename = CJS_COMPAT_NODE_URL.fileURLToPath(import.meta.url); + const __dirname = CJS_COMPAT_NODE_PATH.dirname(__filename); + const require = CJS_COMPAT_NODE_MODULE.createRequire(import.meta.url); + // ------------------------------------------------------------ + // end of CJS compatibility banner, injected by Storybook's esbuild configuration + // ------------------------------------------------------------ + `, }, - }), - entries.browser && - defineESBuildContext('browser', { + plugins: [ + ...sharedOptions.plugins, + metafileWriterPlugin('node', join(DIR_METAFILE_BASE, name)), + ], + }) + ); + } + + if (entries.browser) { + contexts.push( + esbuild.context({ + ...sharedOptions, entryPoints: entries.browser.map(({ entryPoint }) => entryPoint), platform: 'browser', chunkNames: '_browser-chunks/[name]-[hash]', target: BROWSER_TARGETS, supported: SUPPORTED_FEATURES, - }), - entries.runtime && - defineESBuildContext('runtime', { + plugins: [ + ...sharedOptions.plugins, + metafileWriterPlugin('browser', join(DIR_METAFILE_BASE, name)), + ], + }) + ); + } + + if (entries.runtime) { + contexts.push( + esbuild.context({ ...runtimeOptions, entryPoints: entries.runtime.map(({ entryPoint }) => entryPoint), - }), - entries.globalizedRuntime && - defineESBuildContext('globalized-runtime', { + plugins: [ + ...runtimeOptions.plugins, + metafileWriterPlugin('runtime', join(DIR_METAFILE_BASE, name)), + ], + }) + ); + } + + if (entries.globalizedRuntime) { + contexts.push( + esbuild.context({ ...runtimeOptions, entryPoints: entries.globalizedRuntime.map(({ entryPoint }) => entryPoint), - plugins: [globalExternals(globalsModuleInfoMap)], - }), - ].filter(Boolean); - const compile = await Promise.all(contexts.map(([, context]) => context)); - - if (isWatch) { - await Promise.all( - compile.map(async (context) => { - await context.watch(); - await context.rebuild(); + plugins: [ + ...runtimeOptions.plugins, + globalExternals(globalsModuleInfoMap), + metafileWriterPlugin('globalizedRuntime', join(DIR_METAFILE_BASE, name)), + ], }) ); + } - // show a log message when a file is compiled - watch(join(DIR_CWD, 'dist'), { recursive: true }, (_event, filename) => { - console.log(`compiled ${picocolors.cyan(join(DIR_REL, 'dist', filename))}`); - }); - } else { - if (existsSync(DIR_METAFILE)) { - await rm(DIR_METAFILE, { recursive: true, force: true }); - } - await mkdir(DIR_METAFILE, { recursive: true }); + const compile = await Promise.all(contexts); - const nameByIndex = contexts.map(([id]) => id); - const outputs = await Promise.all( - compile.map(async (context) => { - const output = await context.rebuild(); + await Promise.all( + compile.map(async (context) => { + if (isWatch) { + await context.watch(); + // show a log message when a file is compiled + watch(join(DIR_CWD, 'dist'), { recursive: true }, (_event, filename) => { + console.log(`compiled ${picocolors.cyan(join(DIR_REL, 'dist', filename))}`); + }); + } else { + await context.rebuild(); await context.dispose(); - return output; - }) - ); - let index = 0; - for (const currentOutput of outputs) { - index++; - if (!currentOutput.metafile) { - return; } - - await writeFile( - join(DIR_METAFILE, `${nameByIndex[index - 1]}.json`), - JSON.stringify(currentOutput.metafile, null, 2) - ); - } - } + }) + ); } From 31ea1a08ab996db98f9327fe535e058f3b9a348d Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 23 Jul 2025 11:14:13 +0200 Subject: [PATCH 3/9] fix adding storybook config dir to vite config allow list --- code/builders/builder-vite/src/vite-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/builders/builder-vite/src/vite-config.ts b/code/builders/builder-vite/src/vite-config.ts index 520cbabce487..7a4bd5b7d089 100644 --- a/code/builders/builder-vite/src/vite-config.ts +++ b/code/builders/builder-vite/src/vite-config.ts @@ -107,7 +107,7 @@ export async function pluginConfig(options: Options) { // add storybook specific directories only if there's an allow list so that we don't end up // disallowing the root unless root is already disallowed if (config?.server?.fs?.allow) { - config.server.fs.allow.push('.storybook'); + config.server.fs.allow.push(options.configDir); } }, }, From d4c135f325ba6c2a63f6552ded71b7a89db4ffaf Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Wed, 23 Jul 2025 11:14:56 +0200 Subject: [PATCH 4/9] make esbuild analyzer local, support any size of metafile --- code/.storybook/bench.stories.tsx | 94 - code/.storybook/bench/bench.stories.tsx | 61 + .../bench/bundle-analyzer/index.css | 511 ++++ .../bench/bundle-analyzer/index.html | 58 + .../.storybook/bench/bundle-analyzer/index.js | 2073 +++++++++++++++++ code/.storybook/main.ts | 3 +- 6 files changed, 2705 insertions(+), 95 deletions(-) delete mode 100644 code/.storybook/bench.stories.tsx create mode 100644 code/.storybook/bench/bench.stories.tsx create mode 100644 code/.storybook/bench/bundle-analyzer/index.css create mode 100644 code/.storybook/bench/bundle-analyzer/index.html create mode 100644 code/.storybook/bench/bundle-analyzer/index.js diff --git a/code/.storybook/bench.stories.tsx b/code/.storybook/bench.stories.tsx deleted file mode 100644 index 83f999fad215..000000000000 --- a/code/.storybook/bench.stories.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React from 'react'; - -import type { Meta } from '@storybook/react-vite'; - -import { safeMetafileArg } from '../../scripts/bench/safe-args'; - -// @ts-expect-error - TS doesn't know about import.meta.glob from Vite -const allMetafiles = import.meta.glob(['../bench/esbuild-metafiles/**/*.json']); - -export default { - title: 'Bench', - parameters: { - layout: 'fullscreen', - chromatic: { disableSnapshot: true }, - }, - args: { - metafile: safeMetafileArg(Object.keys(allMetafiles)[0]), - }, - argTypes: { - metafile: { - options: Object.keys(allMetafiles).map(safeMetafileArg).sort(), - mapping: Object.fromEntries( - Object.keys(allMetafiles).map((path) => [safeMetafileArg(path), path]) - ), - control: { - type: 'select', - labels: Object.fromEntries( - Object.keys(allMetafiles).map((path) => { - const [, dirName, subEntry] = /esbuild-metafiles\/(.+)\/(.+).json/.exec(path)!; - return [safeMetafileArg(path), `${dirName} - ${subEntry}`]; - }) - ), - }, - }, - }, - loaders: [ - async ({ args }) => { - if (!args.metafile) { - return; - } - let metafile; - try { - metafile = await allMetafiles[args.metafile](); - } catch (e) { - return; - } - const encodedMetafile = btoa(JSON.stringify(metafile)); - return { encodedMetafile }; - }, - ], - render: (args, { loaded }) => { - const { encodedMetafile = '' } = loaded ?? {}; - - if (encodedMetafile.length > 2020836) { - return ( -
-

Metafile is too large

-

- The metafile {args.metafile} is too large to be displayed - in the iframe. This is because we base64-encode the contents of the metafile into the - URL of the {'