|
1 | 1 | import type { Plugin } from 'esbuild'; |
| 2 | +import { readFile } from 'fs-extra'; |
| 3 | +import { dirname } from 'path'; |
2 | 4 |
|
3 | | -export function esbuildPluginCommonjsNamedExports(module: string, namedExports: string[]): Plugin { |
| 5 | +export function esbuildPluginCommonjsNamedExports(modules: string[]): Plugin { |
4 | 6 | return { |
5 | 7 | 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 | + |
8 | 29 | return { |
9 | 30 | path: args.path, |
10 | | - namespace: `commonjs-named-exports-${module}`, |
| 31 | + namespace: 'commonjs-named-exports', |
11 | 32 | pluginData: { |
12 | 33 | resolveDir: args.resolveDir, |
| 34 | + resolvedPath, |
| 35 | + namedExports, |
13 | 36 | }, |
14 | 37 | }; |
15 | 38 | }); |
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 | + |
17 | 57 | 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}';`, |
31 | 60 | }; |
32 | 61 | }); |
| 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 | + } |
33 | 84 | }, |
34 | 85 | }; |
35 | 86 | } |
0 commit comments