-
-
Notifications
You must be signed in to change notification settings - Fork 246
Expand file tree
/
Copy pathcjs.ts
More file actions
98 lines (90 loc) · 3.26 KB
/
cjs.ts
File metadata and controls
98 lines (90 loc) · 3.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import type { Program, Node } from 'estree'
import MagicString from 'magic-string'
import { analyze } from 'periscopic'
import { walk } from 'estree-walker'
// TODO:
// replacing require("xxx") into import("xxx") affects Vite's resolution.
// Runtime helper to handle CJS/ESM interop when transforming require() to import()
// Only unwrap .default for modules that were transformed by this plugin (marked with __cjs_module_runner_transform)
// This ensures we don't incorrectly unwrap .default on genuine ESM modules
const CJS_INTEROP_HELPER = `function __cjs_interop__(m) { return m.__cjs_module_runner_transform ? m.default : m; }`
export function transformCjsToEsm(
code: string,
ast: Program,
): { output: MagicString } {
const output = new MagicString(code)
const analyzed = analyze(ast)
const parentNodes: Node[] = []
const hoistedCodes: string[] = []
let hoistIndex = 0
walk(ast, {
enter(node) {
parentNodes.push(node)
if (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'require' &&
node.arguments.length === 1
) {
let isTopLevel = true
for (const parent of parentNodes) {
if (
parent.type === 'FunctionExpression' ||
parent.type === 'FunctionDeclaration' ||
parent.type === 'ArrowFunctionExpression'
) {
isTopLevel = false
}
// skip locally declared `require`
const scope = analyzed.map.get(parent)
if (scope && scope.declarations.has('require')) {
return
}
}
if (isTopLevel) {
// top-level scope `require` to dynamic import with interop
// (this allows handling react development/production re-export within top-level if branch)
output.update(
node.start,
node.callee.end,
'(__cjs_interop__(await import',
)
output.appendRight(node.end, '))')
} else {
// hoist non top-level `require` to top-level
const hoisted = `__cjs_to_esm_hoist_${hoistIndex}`
const importee = code.slice(
node.arguments[0]!.start,
node.arguments[0]!.end,
)
hoistedCodes.push(
`const ${hoisted} = __cjs_interop__(await import(${importee}));\n`,
)
output.update(node.start, node.end, hoisted)
hoistIndex++
}
}
},
leave() {
parentNodes.pop()!
},
})
// TODO: prepend after shebang
for (const hoisted of hoistedCodes.reverse()) {
output.prepend(hoisted)
}
if (output.hasChanged()) {
output.prepend(`${CJS_INTEROP_HELPER}\n`)
}
// https://nodejs.org/docs/v22.19.0/api/modules.html#exports-shortcut
output.prepend(`let exports = {}; const module = { exports };\n`)
// TODO: can we use cjs-module-lexer to properly define named exports?
// for re-exports, we need to eagerly transform dependencies though.
// https://github.com/nodejs/node/blob/f3adc11e37b8bfaaa026ea85c1cf22e3a0e29ae9/lib/internal/modules/esm/translators.js#L382-L409
output.append(`
;__vite_ssr_exportAll__(module.exports);
export default module.exports;
export const __cjs_module_runner_transform = true;
`)
return { output }
}