Skip to content

Commit cf79dda

Browse files
authored
Merge pull request #2692 from modernweb-dev/fix/storybook-builder-named-exports-improvements
fix(storybook-builder): simplify and speed up the CommonJS to ESM transformation
2 parents 3f5c1e1 + c98a279 commit cf79dda

File tree

5 files changed

+97
-105
lines changed

5 files changed

+97
-105
lines changed

.changeset/warm-poems-watch.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@web/storybook-builder': patch
3+
---
4+
5+
simplify and speed up the CommonJS to ESM transformation
6+
make React conditional reexports work in production

package-lock.json

+10-77
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/storybook-builder/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
"esm"
4848
],
4949
"dependencies": {
50-
"@chialab/esbuild-plugin-commonjs": "^0.17.2",
5150
"@rollup/plugin-node-resolve": "^15.1.0",
5251
"@rollup/pluginutils": "^5.0.2",
5352
"@storybook/core-common": "^7.0.0",
@@ -58,6 +57,7 @@
5857
"@web/dev-server-rollup": "^0.6.1",
5958
"@web/rollup-plugin-html": "^2.3.0",
6059
"browser-assert": "^1.2.1",
60+
"cjs-module-lexer": "^1.2.3",
6161
"es-module-lexer": "^1.2.1",
6262
"esbuild": "^0.19.5",
6363
"express": "^4.18.2",
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,86 @@
11
import type { Plugin } from 'esbuild';
2+
import { readFile } from 'fs-extra';
3+
import { dirname } from 'path';
24

3-
export function esbuildPluginCommonjsNamedExports(module: string, namedExports: string[]): Plugin {
5+
export function esbuildPluginCommonjsNamedExports(modules: string[]): Plugin {
46
return {
57
name: 'commonjs-named-exports',
6-
setup(build) {
7-
build.onResolve({ filter: new RegExp(`^${module}$`) }, args => {
8+
async setup(build) {
9+
const { init, parse } = await import('cjs-module-lexer');
10+
await init();
11+
12+
build.onResolve({ filter: new RegExp(`^(${modules.join('|')})$`) }, async args => {
13+
if (args.pluginData?.preventInfiniteRecursion) return;
14+
15+
const { path, ...rest } = args;
16+
rest.pluginData = { preventInfiniteRecursion: true };
17+
const resolveResult = await build.resolve(path, rest);
18+
const resolvedPath = resolveResult.path;
19+
20+
// skip if resolved to an ESM file
21+
if (resolvedPath.endsWith('.mjs')) return;
22+
23+
const namedExports = await getNamedExports(resolvedPath);
24+
25+
// skip if nothing is exported
26+
// (or was an ESM file with .js extension or just failed)
27+
if (namedExports.length === 0) return;
28+
829
return {
930
path: args.path,
10-
namespace: `commonjs-named-exports-${module}`,
31+
namespace: 'commonjs-named-exports',
1132
pluginData: {
1233
resolveDir: args.resolveDir,
34+
resolvedPath,
35+
namedExports,
1336
},
1437
};
1538
});
16-
build.onLoad({ filter: /.*/, namespace: `commonjs-named-exports-${module}` }, async args => {
39+
40+
build.onLoad({ filter: /.*/, namespace: `commonjs-named-exports` }, async args => {
41+
const { resolveDir, resolvedPath, namedExports } = args.pluginData;
42+
43+
const filteredNamedExports = namedExports.filter((name: string) => {
44+
return (
45+
// interop for "default" export heavily relies on the esbuild work done automatically
46+
// we just always reexport it
47+
// but we need to filter it out here to prevent double reexport if "default" was identified by the lexer
48+
name !== 'default' &&
49+
// we don't need "__esModule" flag in this wrapper
50+
// because it outputs native ESM which will be consumed by other native ESM in the browser
51+
name !== '__esModule'
52+
);
53+
});
54+
55+
const finalExports = ['default', ...filteredNamedExports];
56+
1757
return {
18-
resolveDir: args.pluginData.resolveDir,
19-
contents: `
20-
import { default as commonjsExports } from '${module}?force-original';
21-
${namedExports
22-
.map(name => {
23-
if (name === 'default') {
24-
return `export default commonjsExports;`;
25-
} else {
26-
return `export const ${name} = commonjsExports.${name};`;
27-
}
28-
})
29-
.join('\n')}
30-
`,
58+
resolveDir,
59+
contents: `export { ${finalExports.join(',')} } from '${resolvedPath}';`,
3160
};
3261
});
62+
63+
async function getNamedExports(path: string): Promise<string[]> {
64+
const source = await readFile(path, 'utf8');
65+
66+
let exports: string[] = [];
67+
let reexports: string[] = [];
68+
try {
69+
({ exports, reexports } = parse(source));
70+
} catch (e) {
71+
// good place to start debugging if imports are not working
72+
}
73+
74+
for (const reexport of reexports) {
75+
const reexportPath = require.resolve(reexport, { paths: [dirname(path)] });
76+
const deepExports = await getNamedExports(reexportPath);
77+
for (const deepExport of deepExports) {
78+
exports.push(deepExport);
79+
}
80+
}
81+
82+
return exports;
83+
}
3384
},
3485
};
3586
}

packages/storybook-builder/src/rollup-plugin-prebundle-modules.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,20 @@ export function rollupPluginPrebundleModules(env: Record<string, string>): Plugi
1414
name: 'rollup-plugin-prebundle-modules',
1515

1616
async buildStart() {
17-
const esbuildPluginCommonjs = (await import('@chialab/esbuild-plugin-commonjs')).default; // for CJS compatibility
18-
1917
const modules = CANDIDATES.filter(moduleExists);
2018

2119
for (const module of modules) {
2220
modulePaths[module] = join(
2321
process.cwd(),
2422
PREBUNDLED_MODULES_DIR,
25-
module.endsWith('.js') ? module : `${module}.js`,
23+
module.endsWith('.js') ? module.replace(/\.js$/, '.mjs') : `${module}.mjs`,
2624
);
2725
}
2826

2927
await build({
3028
entryPoints: modules,
3129
outdir: PREBUNDLED_MODULES_DIR,
30+
outExtension: { '.js': '.mjs' },
3231
bundle: true,
3332
format: 'esm',
3433
splitting: true,
@@ -47,12 +46,15 @@ export function rollupPluginPrebundleModules(env: Record<string, string>): Plugi
4746
...stringifyProcessEnvs(env),
4847
},
4948
plugins: [
50-
/* for @storybook/addon-docs */
51-
// tocbot can't be automatically transformed by @chialab/esbuild-plugin-commonjs
52-
// so we need a manual wrapper
53-
esbuildPluginCommonjsNamedExports('tocbot', ['default', 'init', 'destroy']),
54-
55-
esbuildPluginCommonjs(),
49+
esbuildPluginCommonjsNamedExports(
50+
modules.filter(
51+
module =>
52+
// lodash is solved by the lodash-es alias
53+
!module.startsWith('lodash/') &&
54+
// @storybook/react-dom-shim is just an alias to an ESM module
55+
module !== '@storybook/react-dom-shim',
56+
),
57+
),
5658
],
5759
});
5860
},

0 commit comments

Comments
 (0)