@makcbrain/storybook-builder-esbuild 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -0
  3. package/dist/constants.d.ts +1 -0
  4. package/dist/constants.js +1 -0
  5. package/dist/index.d.ts +5 -0
  6. package/dist/index.js +47 -0
  7. package/dist/plugins/reactDocGenPlugin.d.ts +6 -0
  8. package/dist/plugins/reactDocGenPlugin.js +65 -0
  9. package/dist/plugins/virtualModulesPlugin.d.ts +3 -0
  10. package/dist/plugins/virtualModulesPlugin.js +36 -0
  11. package/dist/types.d.ts +21 -0
  12. package/dist/types.js +1 -0
  13. package/dist/utils/actualNameHandler.d.ts +13 -0
  14. package/dist/utils/actualNameHandler.js +30 -0
  15. package/dist/utils/clearDistDirectory.d.ts +2 -0
  16. package/dist/utils/clearDistDirectory.js +6 -0
  17. package/dist/utils/createEsbuildContext.d.ts +3 -0
  18. package/dist/utils/createEsbuildContext.js +6 -0
  19. package/dist/utils/generateAppEntryCode.d.ts +5 -0
  20. package/dist/utils/generateAppEntryCode.js +94 -0
  21. package/dist/utils/generateIframeHtml.d.ts +2 -0
  22. package/dist/utils/generateIframeHtml.js +47 -0
  23. package/dist/utils/generateSetupCode.d.ts +4 -0
  24. package/dist/utils/generateSetupCode.js +11 -0
  25. package/dist/utils/getAbsolutePathToDistDir.d.ts +2 -0
  26. package/dist/utils/getAbsolutePathToDistDir.js +5 -0
  27. package/dist/utils/getEsbuildConfig.d.ts +3 -0
  28. package/dist/utils/getEsbuildConfig.js +66 -0
  29. package/dist/utils/getGlobalExternalsMapping.d.ts +4 -0
  30. package/dist/utils/getGlobalExternalsMapping.js +9 -0
  31. package/dist/utils/getLoaderByFilePath.d.ts +2 -0
  32. package/dist/utils/getLoaderByFilePath.js +15 -0
  33. package/dist/utils/listStories.d.ts +5 -0
  34. package/dist/utils/listStories.js +26 -0
  35. package/package.json +54 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Maksim Kadochnikov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # @makcbrain/storybook-builder-esbuild
2
+
3
+ Storybook builder implementation using esbuild.
4
+
5
+ ## Description
6
+
7
+ This package provides an esbuild-based builder for Storybook. It's used as the underlying build system in `@makcbrain/storybook-framework-react-esbuild`.
8
+
9
+ ## Features
10
+
11
+ - ESM and TypeScript support
12
+ - React JSX transform
13
+ - MDX support
14
+ - Story reload on file changes
15
+ - Configurable source maps
16
+ - Custom esbuild configuration
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install --save-dev @makcbrain/storybook-builder-esbuild
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ This builder is automatically configured when using `@makcbrain/storybook-framework-react-esbuild`.
27
+
28
+ For detailed configuration options and examples, see the [@makcbrain/storybook-framework-react-esbuild](https://www.npmjs.com/package/@makcbrain/storybook-framework-react-esbuild) documentation.
29
+
30
+ ## License
31
+
32
+ MIT
@@ -0,0 +1 @@
1
+ export declare const DIST_DIR_NAME = "esbuild-out";
@@ -0,0 +1 @@
1
+ export const DIST_DIR_NAME = 'esbuild-out';
@@ -0,0 +1,5 @@
1
+ import type { EsbuildBuilder } from './types.ts';
2
+ export type * from './types.ts';
3
+ export declare const bail: () => Promise<void>;
4
+ export declare const start: EsbuildBuilder['start'];
5
+ export declare const build: EsbuildBuilder['build'];
package/dist/index.js ADDED
@@ -0,0 +1,47 @@
1
+ import { clearDistDirectory } from './utils/clearDistDirectory.js';
2
+ import { createEsbuildContext } from './utils/createEsbuildContext.js';
3
+ import { generateIframeHtml } from './utils/generateIframeHtml.js';
4
+ import { getAbsolutePathToDistDir } from './utils/getAbsolutePathToDistDir.js';
5
+ import { listStories } from './utils/listStories.js';
6
+ let ctx;
7
+ export const bail = async () => {
8
+ return ctx?.dispose();
9
+ };
10
+ export const start = async (params) => {
11
+ const { startTime, options, router } = params;
12
+ await clearDistDirectory(options);
13
+ const stories = await listStories(options);
14
+ ctx = await createEsbuildContext(stories, options);
15
+ await ctx.watch();
16
+ const serveResult = await ctx.serve({
17
+ servedir: getAbsolutePathToDistDir(options),
18
+ port: 0, // Auto-select port
19
+ cors: {
20
+ origin: '*',
21
+ },
22
+ });
23
+ const esbuildServerUrl = `http://localhost:${serveResult.port}`;
24
+ console.log(`[ESBuild Builder] Dev server started at ${esbuildServerUrl}`);
25
+ router.get('/iframe.html', async (_req, res) => {
26
+ const html = await generateIframeHtml(options, esbuildServerUrl);
27
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
28
+ res.statusCode = 200;
29
+ res.write(html);
30
+ res.end();
31
+ });
32
+ return {
33
+ bail,
34
+ stats: {
35
+ toJson: () => {
36
+ return {
37
+ message: 'ESBuild stats',
38
+ };
39
+ },
40
+ },
41
+ totalTime: process.hrtime(startTime),
42
+ };
43
+ };
44
+ export const build = async () => {
45
+ // TODO: Implement production build
46
+ console.log('[ESBuild Builder] Production build not yet implemented');
47
+ };
@@ -0,0 +1,6 @@
1
+ import type { Plugin } from 'esbuild';
2
+ /**
3
+ * Plugin for generating React component documentation for @storybook/addon-docs
4
+ * Uses react-docgen which supports both JavaScript and TypeScript
5
+ */
6
+ export declare const reactDocGenPlugin: () => Plugin;
@@ -0,0 +1,65 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { builtinHandlers as docGenHandlers, builtinResolvers as docGenResolver, ERROR_CODES, parse, } from 'react-docgen';
3
+ import { actualNameHandler } from '../utils/actualNameHandler.js';
4
+ import { getLoaderByFilePath } from '../utils/getLoaderByFilePath.js';
5
+ const defaultHandlers = Object.values(docGenHandlers).map((handler) => handler);
6
+ const defaultResolver = new docGenResolver.FindExportedDefinitionsResolver();
7
+ const handlers = [...defaultHandlers, actualNameHandler];
8
+ /**
9
+ * Plugin for generating React component documentation for @storybook/addon-docs
10
+ * Uses react-docgen which supports both JavaScript and TypeScript
11
+ */
12
+ export const reactDocGenPlugin = () => {
13
+ return {
14
+ name: 'react-doc-gen',
15
+ setup(build) {
16
+ build.onLoad({ filter: /\.(mjs|jsx?|tsx?)$/, namespace: '' }, async (args) => {
17
+ if (args.path.includes('node_modules') || args.path.includes('.stories.')) {
18
+ return null;
19
+ }
20
+ try {
21
+ let fileContent = await readFile(args.path, 'utf-8');
22
+ const docGenResults = parse(fileContent, {
23
+ resolver: defaultResolver,
24
+ handlers,
25
+ filename: args.path,
26
+ });
27
+ if (docGenResults.length === 0) {
28
+ return null;
29
+ }
30
+ // Inject __docgenInfo for each component
31
+ for (const info of docGenResults) {
32
+ const { actualName, definedInFile, ...docGenInfo } = info;
33
+ // Only inject if component is defined in this file and has a name
34
+ if (actualName && definedInFile === args.path) {
35
+ const docGenJson = JSON.stringify(docGenInfo);
36
+ fileContent = addDocGenInfo(fileContent, actualName, docGenJson);
37
+ }
38
+ }
39
+ return {
40
+ contents: fileContent,
41
+ loader: getLoaderByFilePath(args.path),
42
+ };
43
+ }
44
+ catch (error) {
45
+ if (typeof error === 'object' &&
46
+ error !== null &&
47
+ 'code' in error &&
48
+ error.code !== ERROR_CODES.MISSING_DEFINITION) {
49
+ console.warn('Error parsing documentation:', error);
50
+ }
51
+ return null;
52
+ }
53
+ });
54
+ },
55
+ };
56
+ };
57
+ function addDocGenInfo(fileContent, actualName, docGenJson) {
58
+ fileContent += `
59
+ try {
60
+ ${actualName}.__docgenInfo=${docGenJson}
61
+ } catch (error) {
62
+ console.warn('Error setting __docgenInfo:', error)
63
+ };`;
64
+ return fileContent;
65
+ }
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from 'esbuild';
2
+ import type { Options } from 'storybook/internal/types';
3
+ export declare const virtualModulesPlugin: (options: Options) => Plugin;
@@ -0,0 +1,36 @@
1
+ import { generateAppEntryCode } from '../utils/generateAppEntryCode.js';
2
+ import { generateSetupCode } from '../utils/generateSetupCode.js';
3
+ export const virtualModulesPlugin = (options) => {
4
+ return {
5
+ name: 'virtual-modules',
6
+ setup(build) {
7
+ build.onResolve({ filter: /^virtualSetup\.js$/ }, () => ({
8
+ path: 'virtualSetup.js',
9
+ namespace: 'virtual',
10
+ }));
11
+ build.onResolve({ filter: /^virtualApp\.js$/ }, () => ({
12
+ path: 'virtualApp.js',
13
+ namespace: 'virtual',
14
+ }));
15
+ build.onLoad({ filter: /.*/, namespace: 'virtual' }, async (args) => {
16
+ if (args.path === 'virtualSetup.js') {
17
+ const code = generateSetupCode();
18
+ return {
19
+ contents: code,
20
+ loader: 'js',
21
+ resolveDir: options.configDir,
22
+ };
23
+ }
24
+ if (args.path === 'virtualApp.js') {
25
+ const code = await generateAppEntryCode(options);
26
+ return {
27
+ contents: code,
28
+ loader: 'js',
29
+ resolveDir: options.configDir,
30
+ };
31
+ }
32
+ return null;
33
+ });
34
+ },
35
+ };
36
+ };
@@ -0,0 +1,21 @@
1
+ import type { BuildOptions } from 'esbuild';
2
+ import type { Builder, Options, Stats } from 'storybook/internal/types';
3
+ type EsbuildStats = Stats;
4
+ export type EsbuildBuilder = Builder<BuildOptions, EsbuildStats>;
5
+ export type EsbuildFinal = (config: BuildOptions, options: Options) => BuildOptions | Promise<BuildOptions>;
6
+ export type StorybookConfigEsbuild = {
7
+ esbuildFinal?: EsbuildFinal;
8
+ };
9
+ export type GetEsbuildConfigProps = {
10
+ isProduction: boolean;
11
+ };
12
+ export type BuilderOptions = {
13
+ /**
14
+ * The easiest and recommended way to provide the esbuild config for your project.
15
+ * The builder will add a few configuration options over your configuration,
16
+ * and it should work out of the box.
17
+ * For some special cases you can use the esbuildFinal property at the top-level of configuration.
18
+ */
19
+ esbuildConfig?: BuildOptions | ((props: GetEsbuildConfigProps) => BuildOptions | Promise<BuildOptions>);
20
+ };
21
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ /**
2
+ * This is heavily based on the react-docgen `displayNameHandler`
3
+ * (https://github.com/reactjs/react-docgen/blob/26c90c0dd105bf83499a83826f2a6ff7a724620d/src/handlers/displayNameHandler.ts)
4
+ * but instead defines an `actualName` property on the generated docs that is taken first from the
5
+ * component's actual name. This addresses an issue where the name that the generated docs are
6
+ * stored under is incorrectly named with the `displayName` and not the component's actual name.
7
+ *
8
+ * This is inspired by `actualNameHandler` from
9
+ * https://github.com/storybookjs/babel-plugin-react-docgen, but is modified directly from
10
+ * displayNameHandler, using the same approach as babel-plugin-react-docgen.
11
+ */
12
+ import type { Handler } from 'react-docgen';
13
+ export declare const actualNameHandler: Handler;
@@ -0,0 +1,30 @@
1
+ import { utils } from 'react-docgen';
2
+ const { getNameOrValue, isReactForwardRefCall } = utils;
3
+ export const actualNameHandler = function actualNameHandler(documentation, componentDefinition) {
4
+ documentation.set('definedInFile', componentDefinition.hub.file.opts.filename);
5
+ if ((componentDefinition.isClassDeclaration() || componentDefinition.isFunctionDeclaration()) &&
6
+ componentDefinition.has('id')) {
7
+ documentation.set('actualName', getNameOrValue(componentDefinition.get('id')));
8
+ }
9
+ else if (componentDefinition.isArrowFunctionExpression() ||
10
+ componentDefinition.isFunctionExpression() ||
11
+ isReactForwardRefCall(componentDefinition)) {
12
+ let currentPath = componentDefinition;
13
+ while (currentPath.parentPath) {
14
+ if (currentPath.parentPath.isVariableDeclarator()) {
15
+ documentation.set('actualName', getNameOrValue(currentPath.parentPath.get('id')));
16
+ return;
17
+ }
18
+ if (currentPath.parentPath.isAssignmentExpression()) {
19
+ const leftPath = currentPath.parentPath.get('left');
20
+ if (leftPath.isIdentifier() || leftPath.isLiteral()) {
21
+ documentation.set('actualName', getNameOrValue(leftPath));
22
+ return;
23
+ }
24
+ }
25
+ currentPath = currentPath.parentPath;
26
+ }
27
+ // Could not find an actual name
28
+ documentation.set('actualName', '');
29
+ }
30
+ };
@@ -0,0 +1,2 @@
1
+ import type { Options } from 'storybook/internal/types';
2
+ export declare const clearDistDirectory: (options: Options) => Promise<void>;
@@ -0,0 +1,6 @@
1
+ import { rm } from 'node:fs/promises';
2
+ import { getAbsolutePathToDistDir } from './getAbsolutePathToDistDir.js';
3
+ export const clearDistDirectory = async (options) => {
4
+ const dirPath = getAbsolutePathToDistDir(options);
5
+ await rm(dirPath, { recursive: true, force: true });
6
+ };
@@ -0,0 +1,3 @@
1
+ import * as esbuild from 'esbuild';
2
+ import type { Options } from 'storybook/internal/types';
3
+ export declare const createEsbuildContext: (stories: string[], options: Options) => Promise<esbuild.BuildContext>;
@@ -0,0 +1,6 @@
1
+ import * as esbuild from 'esbuild';
2
+ import { getEsbuildConfig } from './getEsbuildConfig.js';
3
+ export const createEsbuildContext = async (stories, options) => {
4
+ const config = await getEsbuildConfig(stories, options);
5
+ return esbuild.context(config);
6
+ };
@@ -0,0 +1,5 @@
1
+ import type { Options } from 'storybook/internal/types';
2
+ /**
3
+ * Generate main entry point
4
+ */
5
+ export declare const generateAppEntryCode: (options: Options) => Promise<string>;
@@ -0,0 +1,94 @@
1
+ import path from 'node:path';
2
+ import dedent from 'dedent';
3
+ import { loadPreviewOrConfigFile } from 'storybook/internal/common';
4
+ import { listStories } from './listStories.js';
5
+ const isAnnotationObject = (value) => {
6
+ return typeof value === 'object' && value !== null && 'absolute' in value;
7
+ };
8
+ /**
9
+ * Generate main entry point
10
+ */
11
+ export const generateAppEntryCode = async (options) => {
12
+ const { presets, configDir } = options;
13
+ const stories = await listStories(options);
14
+ // Get preview annotations (.storybook/preview.ts + addons)
15
+ const previewAnnotations = await presets.apply('previewAnnotations', [], options);
16
+ const previewFile = loadPreviewOrConfigFile({ configDir });
17
+ if (previewFile) {
18
+ previewAnnotations.push(previewFile);
19
+ }
20
+ const annotationImports = previewAnnotations
21
+ .map((annotation, index) => {
22
+ const path = isAnnotationObject(annotation) ? annotation.absolute : annotation;
23
+ return `import * as previewAnnotation${index} from '${path}';`;
24
+ })
25
+ .join('\n');
26
+ const configs = previewAnnotations.map((_, index) => `previewAnnotation${index}`).join(', ');
27
+ const storiesImports = stories
28
+ .map((story) => {
29
+ const relative = path.relative(process.cwd(), story);
30
+ const key = story.startsWith('./') ? relative : `./${relative}`;
31
+ // Get CSS file path by replacing story extension with .css
32
+ const cssPath = key.replace(/\.([jt]sx?|mdx)$/, '.css');
33
+ return `'${key}': () => {
34
+ const cssUrl = new URL('${cssPath}', import.meta.url);
35
+
36
+ if (!document.querySelector('link[data-path="${cssPath}"]')) {
37
+ const link = document.createElement('link');
38
+ link.rel = 'stylesheet';
39
+ link.href = cssUrl.href;
40
+ link.setAttribute('data-path', '${cssPath}');
41
+ document.head.appendChild(link);
42
+ }
43
+
44
+ return import('${story}');
45
+ }`;
46
+ })
47
+ .join(',\n ');
48
+ return dedent `
49
+ import { createBrowserChannel } from 'storybook/internal/channels';
50
+ import { addons } from 'storybook/preview-api';
51
+
52
+ const channel = createBrowserChannel({ page: 'preview' });
53
+ addons.setChannel(channel);
54
+ window.__STORYBOOK_ADDONS_CHANNEL__ = channel;
55
+
56
+ if (window.CONFIG_TYPE === 'DEVELOPMENT') {
57
+ window.__STORYBOOK_SERVER_CHANNEL__ = channel;
58
+ }
59
+
60
+ import { composeConfigs, PreviewWeb } from 'storybook/preview-api';
61
+
62
+ // Import preview annotations
63
+ ${annotationImports}
64
+
65
+ // Compose configs
66
+ const getProjectAnnotations = () => {
67
+ return composeConfigs([${configs}]);
68
+ };
69
+
70
+ window.__STORYBOOK_IMPORT_FN__ = (function() {
71
+ const importers = {
72
+ ${storiesImports}
73
+ };
74
+
75
+ return async function importFn(path) {
76
+ const importer = importers[path];
77
+
78
+ if (!importer) {
79
+ throw new Error('Story not found: ' + path + '. Available stories: ' + Object.keys(importers).join(', '));
80
+ }
81
+
82
+ return await importer();
83
+ };
84
+ })();
85
+
86
+ // Initialize PreviewWeb
87
+ window.__STORYBOOK_PREVIEW__ = window.__STORYBOOK_PREVIEW__ || new PreviewWeb(
88
+ window.__STORYBOOK_IMPORT_FN__,
89
+ getProjectAnnotations
90
+ );
91
+
92
+ window.__STORYBOOK_STORY_STORE__ = window.__STORYBOOK_STORY_STORE__ || window.__STORYBOOK_PREVIEW__.storyStore;
93
+ `;
94
+ };
@@ -0,0 +1,2 @@
1
+ import type { Options } from 'storybook/internal/types';
2
+ export declare const generateIframeHtml: (options: Options, esbuildServerUrl: string) => Promise<string>;
@@ -0,0 +1,47 @@
1
+ import dedent from 'dedent';
2
+ export const generateIframeHtml = async (options, esbuildServerUrl) => {
3
+ const { configType, presets } = options;
4
+ const [headHtmlSnippet, bodyHtmlSnippet] = await Promise.all([
5
+ presets.apply('previewHead'),
6
+ presets.apply('previewBody'),
7
+ ]);
8
+ return dedent `
9
+ <!DOCTYPE html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="utf-8" />
13
+ <title>Storybook</title>
14
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
15
+
16
+ <script>
17
+ // Compatibility
18
+ window.module = undefined;
19
+ window.global = window;
20
+ </script>
21
+ ${headHtmlSnippet || ''}
22
+ </head>
23
+ <body>
24
+ ${bodyHtmlSnippet || ''}
25
+ <div id="storybook-root"></div>
26
+ <div id="storybook-docs"></div>
27
+
28
+ <!-- Setup Module - must be loaded first -->
29
+ <script type="module" src="${esbuildServerUrl}/virtualSetup.js"></script>
30
+ <!-- Main Entry Point -->
31
+ <script type="module" src="${esbuildServerUrl}/virtualApp.js"></script>
32
+ <!-- Live Reload (Development Only) -->
33
+ <script>
34
+ if (${configType === 'DEVELOPMENT'}) {
35
+ setTimeout(() => {
36
+ // Timeout to skip first event to prevent reloading at the beginning
37
+ new EventSource('${esbuildServerUrl}/esbuild').addEventListener('change', () => {
38
+ console.log('[ESBuild Builder] File changed, reloading...');
39
+ location.reload();
40
+ });
41
+ }, 1000);
42
+ }
43
+ </script>
44
+ </body>
45
+ </html>
46
+ `;
47
+ };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Generates setup module - must be loaded first to set up Storybook globals
3
+ */
4
+ export declare const generateSetupCode: () => string;
@@ -0,0 +1,11 @@
1
+ import dedent from 'dedent';
2
+ /**
3
+ * Generates setup module - must be loaded first to set up Storybook globals
4
+ */
5
+ export const generateSetupCode = () => {
6
+ return dedent `
7
+ import { setup } from 'storybook/internal/preview/runtime';
8
+
9
+ setup();
10
+ `;
11
+ };
@@ -0,0 +1,2 @@
1
+ import type { Options } from 'storybook/internal/types';
2
+ export declare const getAbsolutePathToDistDir: (options: Options) => string;
@@ -0,0 +1,5 @@
1
+ import { join } from 'node:path';
2
+ import { DIST_DIR_NAME } from '../constants.js';
3
+ export const getAbsolutePathToDistDir = (options) => {
4
+ return join(options.configDir, DIST_DIR_NAME);
5
+ };
@@ -0,0 +1,3 @@
1
+ import type { BuildOptions } from 'esbuild';
2
+ import type { Options } from 'storybook/internal/types';
3
+ export declare const getEsbuildConfig: (stories: string[], options: Options) => Promise<BuildOptions>;
@@ -0,0 +1,66 @@
1
+ import { globalExternals } from '@fal-works/esbuild-plugin-global-externals';
2
+ import getMdxPlugin from '@mdx-js/esbuild';
3
+ import { globalsNameReferenceMap } from 'storybook/internal/preview/globals';
4
+ import { reactDocGenPlugin } from '../plugins/reactDocGenPlugin.js';
5
+ import { virtualModulesPlugin } from '../plugins/virtualModulesPlugin.js';
6
+ import { getAbsolutePathToDistDir } from './getAbsolutePathToDistDir.js';
7
+ import { getGlobalExternalsMapping } from './getGlobalExternalsMapping.js';
8
+ const stringifyEnvs = (envs) => {
9
+ return Object.entries(envs).reduce((acc, [key, value]) => {
10
+ acc[`process.env.${key}`] = JSON.stringify(value);
11
+ return acc;
12
+ }, {});
13
+ };
14
+ export const getEsbuildConfig = async (stories, options) => {
15
+ const { presets } = options;
16
+ const envs = await presets.apply('env');
17
+ const frameworkOptions = await presets.apply('framework');
18
+ const builderOptions = frameworkOptions?.builder;
19
+ let userEsbuildConfig = {};
20
+ if (typeof builderOptions?.esbuildConfig === 'object') {
21
+ userEsbuildConfig = builderOptions?.esbuildConfig;
22
+ }
23
+ else if (typeof builderOptions?.esbuildConfig === 'function') {
24
+ userEsbuildConfig = await builderOptions.esbuildConfig({
25
+ isProduction: options.configType === 'PRODUCTION',
26
+ });
27
+ }
28
+ const config = {
29
+ loader: {
30
+ '.png': 'file',
31
+ '.jpg': 'file',
32
+ '.svg': 'file',
33
+ '.woff': 'file',
34
+ '.woff2': 'file',
35
+ },
36
+ resolveExtensions: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.json'],
37
+ sourcemap: true,
38
+ target: ['esnext'],
39
+ ...userEsbuildConfig,
40
+ entryPoints: [
41
+ 'virtualSetup.js', // Setup module - must be loaded first
42
+ 'virtualApp.js', // Main entry point
43
+ ...stories, // All .stories files
44
+ ],
45
+ outdir: getAbsolutePathToDistDir(options),
46
+ outbase: process.cwd(),
47
+ bundle: true,
48
+ splitting: true,
49
+ format: 'esm',
50
+ define: {
51
+ 'process.env.NODE_ENV': JSON.stringify(options.configType === 'PRODUCTION' ? 'production' : 'development'),
52
+ ...stringifyEnvs(envs),
53
+ ...userEsbuildConfig.define,
54
+ },
55
+ plugins: [
56
+ // Replace Storybook runtime imports with global variables
57
+ // This maps imports like 'storybook/preview-api' to window.__STORYBOOK_MODULE_PREVIEW_API__
58
+ globalExternals(getGlobalExternalsMapping(globalsNameReferenceMap)),
59
+ virtualModulesPlugin(options),
60
+ reactDocGenPlugin(),
61
+ getMdxPlugin({ jsx: true }),
62
+ ...(userEsbuildConfig.plugins || []),
63
+ ],
64
+ };
65
+ return presets.apply('esbuildFinal', config, options);
66
+ };
@@ -0,0 +1,4 @@
1
+ export declare const getGlobalExternalsMapping: (globalsMap: Record<string, string>) => Record<string, {
2
+ varName: string;
3
+ type: "cjs";
4
+ }>;
@@ -0,0 +1,9 @@
1
+ export const getGlobalExternalsMapping = (globalsMap) => {
2
+ return Object.entries(globalsMap).reduce((acc, [moduleName, globalName]) => {
3
+ acc[moduleName] = {
4
+ varName: globalName,
5
+ type: 'cjs',
6
+ };
7
+ return acc;
8
+ }, {});
9
+ };
@@ -0,0 +1,2 @@
1
+ import type { Loader } from 'esbuild';
2
+ export declare const getLoaderByFilePath: (path: string) => Loader | undefined;
@@ -0,0 +1,15 @@
1
+ export const getLoaderByFilePath = (path) => {
2
+ switch (true) {
3
+ case path.endsWith('.jsx'):
4
+ return 'jsx';
5
+ case path.endsWith('.js'):
6
+ case path.endsWith('.mjs'):
7
+ return 'js';
8
+ case path.endsWith('.tsx'):
9
+ return 'tsx';
10
+ case path.endsWith('.ts'):
11
+ return 'ts';
12
+ default:
13
+ return undefined;
14
+ }
15
+ };
@@ -0,0 +1,5 @@
1
+ import type { Options } from 'storybook/internal/types';
2
+ /**
3
+ * Returns absolute paths to stories
4
+ */
5
+ export declare const listStories: (options: Options) => Promise<string[]>;
@@ -0,0 +1,26 @@
1
+ import { isAbsolute, join, resolve } from 'node:path';
2
+ import { glob } from 'glob';
3
+ import slash from 'slash';
4
+ import { commonGlobOptions, normalizeStories } from 'storybook/internal/common';
5
+ /**
6
+ * Returns absolute paths to stories
7
+ */
8
+ export const listStories = async (options) => {
9
+ const storiesGlobs = await options.presets.apply('stories', [], options);
10
+ const normalizedStories = normalizeStories(storiesGlobs, {
11
+ configDir: options.configDir,
12
+ workingDir: process.cwd(),
13
+ });
14
+ return (await Promise.all(normalizedStories.map(async ({ directory, files }) => {
15
+ const absoluteDirectory = isAbsolute(directory)
16
+ ? directory
17
+ : resolve(process.cwd(), directory);
18
+ const absolutePattern = join(absoluteDirectory, files);
19
+ return glob(slash(absolutePattern), {
20
+ ...commonGlobOptions(absolutePattern),
21
+ follow: true,
22
+ });
23
+ })))
24
+ .flat()
25
+ .sort();
26
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@makcbrain/storybook-builder-esbuild",
3
+ "version": "1.0.0",
4
+ "author": "Maksim Kadochnikov <makс.brain@gmail.com>",
5
+ "description": "Builder for Storybook with supporting esbuild",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "keywords": [
9
+ "storybook",
10
+ "esbuild",
11
+ "builder",
12
+ "react"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/makcbrain/storybook-esbuild.git",
17
+ "directory": "packages/builder-esbuild"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/makcbrain/storybook-esbuild/issues"
21
+ },
22
+ "homepage": "https://github.com/makcbrain/storybook-esbuild#readme",
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "main": "./dist/index.js",
27
+ "module": "./dist/index.js",
28
+ "types": "./dist/index.d.ts",
29
+ "files": [
30
+ "dist",
31
+ "assets",
32
+ "LICENSE",
33
+ "package.json"
34
+ ],
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "default": "./dist/index.js"
39
+ },
40
+ "./package.json": "./package.json"
41
+ },
42
+ "scripts": {
43
+ "build": "rm -rf dist && bunx tsc",
44
+ "prepublishOnly": "(cd ../../ && bun run precommit) && bun run build"
45
+ },
46
+ "dependencies": {
47
+ "@fal-works/esbuild-plugin-global-externals": "2.1.2",
48
+ "@mdx-js/esbuild": "3.1.1",
49
+ "dedent": "1.7.0",
50
+ "glob": "11.0.3",
51
+ "react-docgen": "8.0.2",
52
+ "slash": "5.1.0"
53
+ }
54
+ }