@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.
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +47 -0
- package/dist/plugins/reactDocGenPlugin.d.ts +6 -0
- package/dist/plugins/reactDocGenPlugin.js +65 -0
- package/dist/plugins/virtualModulesPlugin.d.ts +3 -0
- package/dist/plugins/virtualModulesPlugin.js +36 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +1 -0
- package/dist/utils/actualNameHandler.d.ts +13 -0
- package/dist/utils/actualNameHandler.js +30 -0
- package/dist/utils/clearDistDirectory.d.ts +2 -0
- package/dist/utils/clearDistDirectory.js +6 -0
- package/dist/utils/createEsbuildContext.d.ts +3 -0
- package/dist/utils/createEsbuildContext.js +6 -0
- package/dist/utils/generateAppEntryCode.d.ts +5 -0
- package/dist/utils/generateAppEntryCode.js +94 -0
- package/dist/utils/generateIframeHtml.d.ts +2 -0
- package/dist/utils/generateIframeHtml.js +47 -0
- package/dist/utils/generateSetupCode.d.ts +4 -0
- package/dist/utils/generateSetupCode.js +11 -0
- package/dist/utils/getAbsolutePathToDistDir.d.ts +2 -0
- package/dist/utils/getAbsolutePathToDistDir.js +5 -0
- package/dist/utils/getEsbuildConfig.d.ts +3 -0
- package/dist/utils/getEsbuildConfig.js +66 -0
- package/dist/utils/getGlobalExternalsMapping.d.ts +4 -0
- package/dist/utils/getGlobalExternalsMapping.js +9 -0
- package/dist/utils/getLoaderByFilePath.d.ts +2 -0
- package/dist/utils/getLoaderByFilePath.js +15 -0
- package/dist/utils/listStories.d.ts +5 -0
- package/dist/utils/listStories.js +26 -0
- 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';
|
package/dist/index.d.ts
ADDED
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,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,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
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -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,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,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,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,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,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,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,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
|
+
}
|