@scelar/nodepod 1.0.2 → 1.0.4
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/dist/__sw__.js +642 -642
- package/dist/__tests__/bench/integration.bench.d.ts +1 -0
- package/dist/__tests__/bench/memory-volume.bench.d.ts +1 -0
- package/dist/__tests__/bench/polyfills.bench.d.ts +1 -0
- package/dist/__tests__/bench/script-engine.bench.d.ts +1 -0
- package/dist/__tests__/bench/shell.bench.d.ts +1 -0
- package/dist/__tests__/bench/syntax-transforms.bench.d.ts +1 -0
- package/dist/__tests__/bench/version-resolver.bench.d.ts +1 -0
- package/dist/__tests__/buffer.test.d.ts +1 -0
- package/dist/__tests__/byte-encoding.test.d.ts +1 -0
- package/dist/__tests__/digest.test.d.ts +1 -0
- package/dist/__tests__/events.test.d.ts +1 -0
- package/dist/__tests__/memory-volume.test.d.ts +1 -0
- package/dist/__tests__/path.test.d.ts +1 -0
- package/dist/__tests__/process.test.d.ts +1 -0
- package/dist/__tests__/script-engine.test.d.ts +1 -0
- package/dist/__tests__/shell-builtins.test.d.ts +1 -0
- package/dist/__tests__/shell-interpreter.test.d.ts +1 -0
- package/dist/__tests__/shell-parser.test.d.ts +1 -0
- package/dist/__tests__/stream.test.d.ts +1 -0
- package/dist/__tests__/syntax-transforms.test.d.ts +1 -0
- package/dist/__tests__/version-resolver.test.d.ts +1 -0
- package/dist/{child_process-Dopvyd-E.js → child_process-53fMkug_.js} +4 -4
- package/dist/child_process-53fMkug_.js.map +1 -0
- package/dist/{child_process-B38qoN6R.cjs → child_process-lxSKECHq.cjs} +5 -5
- package/dist/child_process-lxSKECHq.cjs.map +1 -0
- package/dist/{index--Qr8LVpQ.js → index-B8lyh_ti.js} +1316 -559
- package/dist/index-B8lyh_ti.js.map +1 -0
- package/dist/{index-cnitc68U.cjs → index-C-TQIrdG.cjs} +1422 -612
- package/dist/index-C-TQIrdG.cjs.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/memory-volume.d.ts +1 -1
- package/dist/polyfills/wasi.d.ts +45 -4
- package/dist/script-engine.d.ts +2 -0
- package/dist/sdk/nodepod.d.ts +4 -3
- package/dist/sdk/types.d.ts +6 -0
- package/dist/syntax-transforms.d.ts +1 -0
- package/dist/threading/process-manager.d.ts +1 -1
- package/dist/threading/worker-protocol.d.ts +1 -1
- package/package.json +5 -3
- package/src/__tests__/bench/integration.bench.ts +117 -0
- package/src/__tests__/bench/memory-volume.bench.ts +115 -0
- package/src/__tests__/bench/polyfills.bench.ts +147 -0
- package/src/__tests__/bench/script-engine.bench.ts +104 -0
- package/src/__tests__/bench/shell.bench.ts +101 -0
- package/src/__tests__/bench/syntax-transforms.bench.ts +82 -0
- package/src/__tests__/bench/version-resolver.bench.ts +95 -0
- package/src/__tests__/buffer.test.ts +273 -0
- package/src/__tests__/byte-encoding.test.ts +98 -0
- package/src/__tests__/digest.test.ts +44 -0
- package/src/__tests__/events.test.ts +245 -0
- package/src/__tests__/memory-volume.test.ts +443 -0
- package/src/__tests__/path.test.ts +181 -0
- package/src/__tests__/process.test.ts +129 -0
- package/src/__tests__/script-engine.test.ts +229 -0
- package/src/__tests__/shell-builtins.test.ts +357 -0
- package/src/__tests__/shell-interpreter.test.ts +157 -0
- package/src/__tests__/shell-parser.test.ts +204 -0
- package/src/__tests__/stream.test.ts +142 -0
- package/src/__tests__/syntax-transforms.test.ts +158 -0
- package/src/__tests__/version-resolver.test.ts +184 -0
- package/src/constants/cdn-urls.ts +18 -18
- package/src/helpers/byte-encoding.ts +51 -39
- package/src/index.ts +192 -192
- package/src/memory-volume.ts +968 -941
- package/src/module-transformer.ts +368 -368
- package/src/packages/installer.ts +396 -396
- package/src/packages/version-resolver.ts +12 -2
- package/src/polyfills/buffer.ts +633 -628
- package/src/polyfills/child_process.ts +2288 -2288
- package/src/polyfills/esbuild.ts +854 -854
- package/src/polyfills/events.ts +282 -276
- package/src/polyfills/fs.ts +2888 -2888
- package/src/polyfills/http.ts +1450 -1449
- package/src/polyfills/process.ts +721 -690
- package/src/polyfills/readline.ts +692 -692
- package/src/polyfills/stream.ts +1620 -1620
- package/src/polyfills/tty.ts +71 -71
- package/src/polyfills/wasi.ts +1284 -22
- package/src/request-proxy.ts +716 -716
- package/src/script-engine.ts +465 -146
- package/src/sdk/nodepod.ts +525 -509
- package/src/sdk/types.ts +7 -0
- package/src/syntax-transforms.ts +543 -561
- package/src/threading/offload-worker.ts +383 -383
- package/src/threading/offload.ts +271 -271
- package/src/threading/process-manager.ts +956 -956
- package/src/threading/process-worker-entry.ts +858 -854
- package/src/threading/worker-protocol.ts +1 -1
- package/dist/child_process-B38qoN6R.cjs.map +0 -1
- package/dist/child_process-Dopvyd-E.js.map +0 -1
- package/dist/index--Qr8LVpQ.js.map +0 -1
- package/dist/index-cnitc68U.cjs.map +0 -1
|
@@ -1,368 +1,368 @@
|
|
|
1
|
-
// ESM-to-CJS bulk transformer (esbuild-wasm). Used at install time to
|
|
2
|
-
// pre-convert packages so synchronous require() works at runtime.
|
|
3
|
-
// Transforms are offloaded to Web Workers when available.
|
|
4
|
-
|
|
5
|
-
import type { MemoryVolume } from "./memory-volume";
|
|
6
|
-
import { CDN_ESBUILD_ESM, CDN_ESBUILD_BINARY, cdnImport } from "./constants/cdn-urls";
|
|
7
|
-
import { offload, taskId, TaskPriority } from "./threading/offload";
|
|
8
|
-
import type { TransformResult } from "./threading/offload-types";
|
|
9
|
-
|
|
10
|
-
const inBrowser = typeof window !== "undefined";
|
|
11
|
-
|
|
12
|
-
// load and init esbuild-wasm from CDN (reuses existing instance)
|
|
13
|
-
export async function prepareTransformer(): Promise<void> {
|
|
14
|
-
if (!inBrowser) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (window.__esbuildEngine) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (window.__esbuildReady) {
|
|
23
|
-
return window.__esbuildReady;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
window.__esbuildReady = (async () => {
|
|
27
|
-
try {
|
|
28
|
-
const loaded = await cdnImport(CDN_ESBUILD_ESM);
|
|
29
|
-
const engine = loaded.default || loaded;
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
await engine.initialize({ wasmURL: CDN_ESBUILD_BINARY });
|
|
33
|
-
} catch (initErr) {
|
|
34
|
-
if (
|
|
35
|
-
initErr instanceof Error &&
|
|
36
|
-
initErr.message.includes('Cannot call "initialize" more than once')
|
|
37
|
-
) {
|
|
38
|
-
/* already initialized, ignore */
|
|
39
|
-
} else {
|
|
40
|
-
throw initErr;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
window.__esbuildEngine = engine;
|
|
45
|
-
} catch (err) {
|
|
46
|
-
window.__esbuildReady = undefined;
|
|
47
|
-
throw err;
|
|
48
|
-
}
|
|
49
|
-
})();
|
|
50
|
-
|
|
51
|
-
return window.__esbuildReady;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function isTransformerLoaded(): boolean {
|
|
55
|
-
if (!inBrowser) return true;
|
|
56
|
-
return window.__esbuildEngine !== undefined;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function containsJsx(source: string): boolean {
|
|
60
|
-
if (/<[A-Z][a-zA-Z0-9.]*[\s/>]/.test(source)) return true;
|
|
61
|
-
if (/<\/[a-zA-Z]/.test(source)) return true;
|
|
62
|
-
if (/\/>/.test(source)) return true;
|
|
63
|
-
if (/<>|<\/>/.test(source)) return true;
|
|
64
|
-
if (/React\.createElement\b/.test(source)) return true;
|
|
65
|
-
if (/jsx\(|jsxs\(|jsxDEV\(/.test(source)) return true;
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function detectLoader(
|
|
70
|
-
filePath: string,
|
|
71
|
-
source: string,
|
|
72
|
-
): "js" | "jsx" | "ts" | "tsx" {
|
|
73
|
-
if (filePath.endsWith(".jsx")) return "jsx";
|
|
74
|
-
if (filePath.endsWith(".ts")) return "ts";
|
|
75
|
-
if (filePath.endsWith(".tsx")) return "tsx";
|
|
76
|
-
if (filePath.endsWith(".mjs")) return "js";
|
|
77
|
-
if (containsJsx(source)) return "jsx";
|
|
78
|
-
return "js";
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// convert a single file ESM->CJS via offloaded esbuild.transform()
|
|
82
|
-
export async function convertFile(
|
|
83
|
-
source: string,
|
|
84
|
-
filePath: string,
|
|
85
|
-
): Promise<string> {
|
|
86
|
-
if (!inBrowser) return source;
|
|
87
|
-
|
|
88
|
-
const result: TransformResult = await offload({
|
|
89
|
-
type: "transform",
|
|
90
|
-
id: taskId(),
|
|
91
|
-
source,
|
|
92
|
-
filePath,
|
|
93
|
-
options: {
|
|
94
|
-
loader: detectLoader(filePath, source),
|
|
95
|
-
format: "cjs",
|
|
96
|
-
target: "esnext",
|
|
97
|
-
platform: "neutral",
|
|
98
|
-
define: {
|
|
99
|
-
"import.meta.url": "import_meta.url",
|
|
100
|
-
"import.meta.dirname": "import_meta.dirname",
|
|
101
|
-
"import.meta.filename": "import_meta.filename",
|
|
102
|
-
"import.meta": "import_meta",
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
priority: TaskPriority.NORMAL,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// patch dynamic imports of node builtins to synchronous require()
|
|
109
|
-
let code = result.code;
|
|
110
|
-
code = code.replace(
|
|
111
|
-
/\bimport\s*\(\s*["']node:([^"']+)["']\s*\)/g,
|
|
112
|
-
'Promise.resolve(require("node:$1"))',
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
const coreModules = [
|
|
116
|
-
"assert", "buffer", "child_process", "cluster", "crypto",
|
|
117
|
-
"dgram", "dns", "events", "fs", "http", "http2", "https",
|
|
118
|
-
"net", "os", "path", "perf_hooks", "querystring", "readline",
|
|
119
|
-
"stream", "string_decoder", "timers", "tls", "url", "util",
|
|
120
|
-
"v8", "vm", "worker_threads", "zlib", "async_hooks",
|
|
121
|
-
"inspector", "module",
|
|
122
|
-
];
|
|
123
|
-
for (const mod of coreModules) {
|
|
124
|
-
const re = new RegExp(`\\bimport\\s*\\(\\s*["']${mod}["']\\s*\\)`, "g");
|
|
125
|
-
code = code.replace(re, `Promise.resolve(require("${mod}"))`);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return code;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// direct main-thread esbuild transform (no worker offloading).
|
|
132
|
-
// used as the fallback path to avoid circular offload -> convertFile -> offload.
|
|
133
|
-
export async function convertFileDirect(
|
|
134
|
-
source: string,
|
|
135
|
-
filePath: string,
|
|
136
|
-
): Promise<string> {
|
|
137
|
-
if (!inBrowser) return source;
|
|
138
|
-
|
|
139
|
-
if (!window.__esbuildEngine) await prepareTransformer();
|
|
140
|
-
const engine = window.__esbuildEngine;
|
|
141
|
-
if (!engine) throw new Error("esbuild engine not available");
|
|
142
|
-
|
|
143
|
-
const loader = detectLoader(filePath, source);
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
const output = await engine.transform(source, {
|
|
147
|
-
loader,
|
|
148
|
-
format: "cjs",
|
|
149
|
-
target: "esnext",
|
|
150
|
-
platform: "neutral",
|
|
151
|
-
define: {
|
|
152
|
-
"import.meta.url": "import_meta.url",
|
|
153
|
-
"import.meta.dirname": "import_meta.dirname",
|
|
154
|
-
"import.meta.filename": "import_meta.filename",
|
|
155
|
-
"import.meta": "import_meta",
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
return output.code;
|
|
159
|
-
} catch (err: unknown) {
|
|
160
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
161
|
-
|
|
162
|
-
// retry with alternative loaders
|
|
163
|
-
if (loader === "js" || loader === "jsx") {
|
|
164
|
-
const fallbacks = loader === "js" ? ["jsx", "tsx", "ts"] : ["tsx"];
|
|
165
|
-
for (const fallbackLoader of fallbacks) {
|
|
166
|
-
try {
|
|
167
|
-
const output = await engine.transform(source, {
|
|
168
|
-
loader: fallbackLoader as "jsx" | "tsx" | "ts",
|
|
169
|
-
format: "cjs",
|
|
170
|
-
target: "esnext",
|
|
171
|
-
platform: "neutral",
|
|
172
|
-
define: {
|
|
173
|
-
"import.meta.url": "import_meta.url",
|
|
174
|
-
"import.meta.dirname": "import_meta.dirname",
|
|
175
|
-
"import.meta.filename": "import_meta.filename",
|
|
176
|
-
"import.meta": "import_meta",
|
|
177
|
-
},
|
|
178
|
-
});
|
|
179
|
-
return output.code;
|
|
180
|
-
} catch {
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (msg.includes("Top-level await")) {
|
|
186
|
-
try {
|
|
187
|
-
const output = await engine.transform(source, {
|
|
188
|
-
loader,
|
|
189
|
-
format: "esm",
|
|
190
|
-
target: "esnext",
|
|
191
|
-
platform: "neutral",
|
|
192
|
-
define: {
|
|
193
|
-
"import.meta.url": "import_meta.url",
|
|
194
|
-
"import.meta.dirname": "import_meta.dirname",
|
|
195
|
-
"import.meta.filename": "import_meta.filename",
|
|
196
|
-
"import.meta": "import_meta",
|
|
197
|
-
},
|
|
198
|
-
});
|
|
199
|
-
return output.code;
|
|
200
|
-
} catch {
|
|
201
|
-
return source;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return source;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function requiresConversion(filePath: string, source: string): boolean {
|
|
209
|
-
// .mjs must stay ESM in the VFS -- tools read them directly. the runtime
|
|
210
|
-
// handles ESM->CJS on-the-fly for require().
|
|
211
|
-
if (filePath.endsWith(".mjs")) return false;
|
|
212
|
-
if (filePath.endsWith(".cjs")) return false;
|
|
213
|
-
return (
|
|
214
|
-
/\bimport\s+[\w{*'"]/m.test(source) ||
|
|
215
|
-
/\bexport\s+(?:default|const|let|var|function|class|{|\*)/m.test(source) ||
|
|
216
|
-
/\bimport\.meta\b/.test(source)
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function needsBuiltinPatching(source: string): boolean {
|
|
221
|
-
return (
|
|
222
|
-
/\bimport\s*\(\s*["']node:/.test(source) ||
|
|
223
|
-
/\bimport\s*\(\s*["'](fs|path|http|https|net|url|util|events|stream|os|crypto)["']/.test(
|
|
224
|
-
source,
|
|
225
|
-
)
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function patchBuiltinImports(source: string): string {
|
|
230
|
-
let patched = source;
|
|
231
|
-
patched = patched.replace(
|
|
232
|
-
/\bimport\s*\(\s*["']node:([^"']+)["']\s*\)/g,
|
|
233
|
-
'Promise.resolve(require("node:$1"))',
|
|
234
|
-
);
|
|
235
|
-
const builtins = [
|
|
236
|
-
"assert",
|
|
237
|
-
"buffer",
|
|
238
|
-
"child_process",
|
|
239
|
-
"cluster",
|
|
240
|
-
"crypto",
|
|
241
|
-
"dgram",
|
|
242
|
-
"dns",
|
|
243
|
-
"events",
|
|
244
|
-
"fs",
|
|
245
|
-
"http",
|
|
246
|
-
"http2",
|
|
247
|
-
"https",
|
|
248
|
-
"net",
|
|
249
|
-
"os",
|
|
250
|
-
"path",
|
|
251
|
-
"perf_hooks",
|
|
252
|
-
"querystring",
|
|
253
|
-
"readline",
|
|
254
|
-
"stream",
|
|
255
|
-
"string_decoder",
|
|
256
|
-
"timers",
|
|
257
|
-
"tls",
|
|
258
|
-
"url",
|
|
259
|
-
"util",
|
|
260
|
-
"v8",
|
|
261
|
-
"vm",
|
|
262
|
-
"worker_threads",
|
|
263
|
-
"zlib",
|
|
264
|
-
"async_hooks",
|
|
265
|
-
"inspector",
|
|
266
|
-
"module",
|
|
267
|
-
];
|
|
268
|
-
for (const b of builtins) {
|
|
269
|
-
patched = patched.replace(
|
|
270
|
-
new RegExp(`\\bimport\\s*\\(\\s*["']${b}["']\\s*\\)`, "g"),
|
|
271
|
-
`Promise.resolve(require("${b}"))`,
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
return patched;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function listJsFiles(vol: MemoryVolume, dir: string): string[] {
|
|
278
|
-
const result: string[] = [];
|
|
279
|
-
try {
|
|
280
|
-
for (const name of vol.readdirSync(dir)) {
|
|
281
|
-
const full = dir + "/" + name;
|
|
282
|
-
try {
|
|
283
|
-
const info = vol.statSync(full);
|
|
284
|
-
if (info.isDirectory()) {
|
|
285
|
-
if (name !== "node_modules") result.push(...listJsFiles(vol, full));
|
|
286
|
-
} else if (
|
|
287
|
-
name.endsWith(".js") ||
|
|
288
|
-
name.endsWith(".mjs") ||
|
|
289
|
-
name.endsWith(".jsx")
|
|
290
|
-
) {
|
|
291
|
-
result.push(full);
|
|
292
|
-
}
|
|
293
|
-
} catch {
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
} catch {
|
|
297
|
-
}
|
|
298
|
-
return result;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function isEsmPackage(vol: MemoryVolume, packageDir: string): boolean {
|
|
302
|
-
try {
|
|
303
|
-
const pkgPath = packageDir + "/package.json";
|
|
304
|
-
if (vol.existsSync(pkgPath)) {
|
|
305
|
-
const raw = vol.readFileSync(pkgPath, "utf8");
|
|
306
|
-
const pkg = JSON.parse(raw);
|
|
307
|
-
return pkg.type === "module";
|
|
308
|
-
}
|
|
309
|
-
} catch {
|
|
310
|
-
}
|
|
311
|
-
return false;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// convert all ESM files in a package directory to CJS
|
|
315
|
-
export async function convertPackage(
|
|
316
|
-
vol: MemoryVolume,
|
|
317
|
-
packageDir: string,
|
|
318
|
-
onProgress?: (msg: string) => void,
|
|
319
|
-
): Promise<number> {
|
|
320
|
-
let converted = 0;
|
|
321
|
-
const files = listJsFiles(vol, packageDir);
|
|
322
|
-
onProgress?.(` Converting ${files.length} files in ${packageDir}...`);
|
|
323
|
-
|
|
324
|
-
// "type":"module" packages stay as ESM in VFS -- tools like Vite read them
|
|
325
|
-
// directly and expect ESM syntax. require() handles conversion at runtime.
|
|
326
|
-
const esmPackage = isEsmPackage(vol, packageDir);
|
|
327
|
-
|
|
328
|
-
const BATCH = 50;
|
|
329
|
-
for (let i = 0; i < files.length; i += BATCH) {
|
|
330
|
-
const batch = files.slice(i, i + BATCH);
|
|
331
|
-
await Promise.all(
|
|
332
|
-
batch.map(async (fp) => {
|
|
333
|
-
try {
|
|
334
|
-
const src = vol.readFileSync(fp, "utf8");
|
|
335
|
-
|
|
336
|
-
if (esmPackage) {
|
|
337
|
-
if (needsBuiltinPatching(src)) {
|
|
338
|
-
vol.writeFileSync(fp, patchBuiltinImports(src));
|
|
339
|
-
converted++;
|
|
340
|
-
}
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// .mjs stays ESM even in non-ESM packages
|
|
345
|
-
if (fp.endsWith(".mjs")) {
|
|
346
|
-
if (needsBuiltinPatching(src)) {
|
|
347
|
-
vol.writeFileSync(fp, patchBuiltinImports(src));
|
|
348
|
-
converted++;
|
|
349
|
-
}
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (requiresConversion(fp, src)) {
|
|
354
|
-
const out = await convertFile(src, fp);
|
|
355
|
-
vol.writeFileSync(fp, out);
|
|
356
|
-
converted++;
|
|
357
|
-
} else if (needsBuiltinPatching(src)) {
|
|
358
|
-
vol.writeFileSync(fp, patchBuiltinImports(src));
|
|
359
|
-
converted++;
|
|
360
|
-
}
|
|
361
|
-
} catch {
|
|
362
|
-
}
|
|
363
|
-
}),
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return converted;
|
|
368
|
-
}
|
|
1
|
+
// ESM-to-CJS bulk transformer (esbuild-wasm). Used at install time to
|
|
2
|
+
// pre-convert packages so synchronous require() works at runtime.
|
|
3
|
+
// Transforms are offloaded to Web Workers when available.
|
|
4
|
+
|
|
5
|
+
import type { MemoryVolume } from "./memory-volume";
|
|
6
|
+
import { CDN_ESBUILD_ESM, CDN_ESBUILD_BINARY, cdnImport } from "./constants/cdn-urls";
|
|
7
|
+
import { offload, taskId, TaskPriority } from "./threading/offload";
|
|
8
|
+
import type { TransformResult } from "./threading/offload-types";
|
|
9
|
+
|
|
10
|
+
const inBrowser = typeof window !== "undefined";
|
|
11
|
+
|
|
12
|
+
// load and init esbuild-wasm from CDN (reuses existing instance)
|
|
13
|
+
export async function prepareTransformer(): Promise<void> {
|
|
14
|
+
if (!inBrowser) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (window.__esbuildEngine) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (window.__esbuildReady) {
|
|
23
|
+
return window.__esbuildReady;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
window.__esbuildReady = (async () => {
|
|
27
|
+
try {
|
|
28
|
+
const loaded = await cdnImport(CDN_ESBUILD_ESM);
|
|
29
|
+
const engine = loaded.default || loaded;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await engine.initialize({ wasmURL: CDN_ESBUILD_BINARY });
|
|
33
|
+
} catch (initErr) {
|
|
34
|
+
if (
|
|
35
|
+
initErr instanceof Error &&
|
|
36
|
+
initErr.message.includes('Cannot call "initialize" more than once')
|
|
37
|
+
) {
|
|
38
|
+
/* already initialized, ignore */
|
|
39
|
+
} else {
|
|
40
|
+
throw initErr;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
window.__esbuildEngine = engine;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
window.__esbuildReady = undefined;
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
|
|
51
|
+
return window.__esbuildReady;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isTransformerLoaded(): boolean {
|
|
55
|
+
if (!inBrowser) return true;
|
|
56
|
+
return window.__esbuildEngine !== undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function containsJsx(source: string): boolean {
|
|
60
|
+
if (/<[A-Z][a-zA-Z0-9.]*[\s/>]/.test(source)) return true;
|
|
61
|
+
if (/<\/[a-zA-Z]/.test(source)) return true;
|
|
62
|
+
if (/\/>/.test(source)) return true;
|
|
63
|
+
if (/<>|<\/>/.test(source)) return true;
|
|
64
|
+
if (/React\.createElement\b/.test(source)) return true;
|
|
65
|
+
if (/jsx\(|jsxs\(|jsxDEV\(/.test(source)) return true;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function detectLoader(
|
|
70
|
+
filePath: string,
|
|
71
|
+
source: string,
|
|
72
|
+
): "js" | "jsx" | "ts" | "tsx" {
|
|
73
|
+
if (filePath.endsWith(".jsx")) return "jsx";
|
|
74
|
+
if (filePath.endsWith(".ts")) return "ts";
|
|
75
|
+
if (filePath.endsWith(".tsx")) return "tsx";
|
|
76
|
+
if (filePath.endsWith(".mjs")) return "js";
|
|
77
|
+
if (containsJsx(source)) return "jsx";
|
|
78
|
+
return "js";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// convert a single file ESM->CJS via offloaded esbuild.transform()
|
|
82
|
+
export async function convertFile(
|
|
83
|
+
source: string,
|
|
84
|
+
filePath: string,
|
|
85
|
+
): Promise<string> {
|
|
86
|
+
if (!inBrowser) return source;
|
|
87
|
+
|
|
88
|
+
const result: TransformResult = await offload({
|
|
89
|
+
type: "transform",
|
|
90
|
+
id: taskId(),
|
|
91
|
+
source,
|
|
92
|
+
filePath,
|
|
93
|
+
options: {
|
|
94
|
+
loader: detectLoader(filePath, source),
|
|
95
|
+
format: "cjs",
|
|
96
|
+
target: "esnext",
|
|
97
|
+
platform: "neutral",
|
|
98
|
+
define: {
|
|
99
|
+
"import.meta.url": "import_meta.url",
|
|
100
|
+
"import.meta.dirname": "import_meta.dirname",
|
|
101
|
+
"import.meta.filename": "import_meta.filename",
|
|
102
|
+
"import.meta": "import_meta",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
priority: TaskPriority.NORMAL,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// patch dynamic imports of node builtins to synchronous require()
|
|
109
|
+
let code = result.code;
|
|
110
|
+
code = code.replace(
|
|
111
|
+
/\bimport\s*\(\s*["']node:([^"']+)["']\s*\)/g,
|
|
112
|
+
'Promise.resolve(require("node:$1"))',
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const coreModules = [
|
|
116
|
+
"assert", "buffer", "child_process", "cluster", "crypto",
|
|
117
|
+
"dgram", "dns", "events", "fs", "http", "http2", "https",
|
|
118
|
+
"net", "os", "path", "perf_hooks", "querystring", "readline",
|
|
119
|
+
"stream", "string_decoder", "timers", "tls", "url", "util",
|
|
120
|
+
"v8", "vm", "worker_threads", "zlib", "async_hooks",
|
|
121
|
+
"inspector", "module",
|
|
122
|
+
];
|
|
123
|
+
for (const mod of coreModules) {
|
|
124
|
+
const re = new RegExp(`\\bimport\\s*\\(\\s*["']${mod}["']\\s*\\)`, "g");
|
|
125
|
+
code = code.replace(re, `Promise.resolve(require("${mod}"))`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return code;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// direct main-thread esbuild transform (no worker offloading).
|
|
132
|
+
// used as the fallback path to avoid circular offload -> convertFile -> offload.
|
|
133
|
+
export async function convertFileDirect(
|
|
134
|
+
source: string,
|
|
135
|
+
filePath: string,
|
|
136
|
+
): Promise<string> {
|
|
137
|
+
if (!inBrowser) return source;
|
|
138
|
+
|
|
139
|
+
if (!window.__esbuildEngine) await prepareTransformer();
|
|
140
|
+
const engine = window.__esbuildEngine;
|
|
141
|
+
if (!engine) throw new Error("esbuild engine not available");
|
|
142
|
+
|
|
143
|
+
const loader = detectLoader(filePath, source);
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const output = await engine.transform(source, {
|
|
147
|
+
loader,
|
|
148
|
+
format: "cjs",
|
|
149
|
+
target: "esnext",
|
|
150
|
+
platform: "neutral",
|
|
151
|
+
define: {
|
|
152
|
+
"import.meta.url": "import_meta.url",
|
|
153
|
+
"import.meta.dirname": "import_meta.dirname",
|
|
154
|
+
"import.meta.filename": "import_meta.filename",
|
|
155
|
+
"import.meta": "import_meta",
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
return output.code;
|
|
159
|
+
} catch (err: unknown) {
|
|
160
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
161
|
+
|
|
162
|
+
// retry with alternative loaders
|
|
163
|
+
if (loader === "js" || loader === "jsx") {
|
|
164
|
+
const fallbacks = loader === "js" ? ["jsx", "tsx", "ts"] : ["tsx"];
|
|
165
|
+
for (const fallbackLoader of fallbacks) {
|
|
166
|
+
try {
|
|
167
|
+
const output = await engine.transform(source, {
|
|
168
|
+
loader: fallbackLoader as "jsx" | "tsx" | "ts",
|
|
169
|
+
format: "cjs",
|
|
170
|
+
target: "esnext",
|
|
171
|
+
platform: "neutral",
|
|
172
|
+
define: {
|
|
173
|
+
"import.meta.url": "import_meta.url",
|
|
174
|
+
"import.meta.dirname": "import_meta.dirname",
|
|
175
|
+
"import.meta.filename": "import_meta.filename",
|
|
176
|
+
"import.meta": "import_meta",
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
return output.code;
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (msg.includes("Top-level await")) {
|
|
186
|
+
try {
|
|
187
|
+
const output = await engine.transform(source, {
|
|
188
|
+
loader,
|
|
189
|
+
format: "esm",
|
|
190
|
+
target: "esnext",
|
|
191
|
+
platform: "neutral",
|
|
192
|
+
define: {
|
|
193
|
+
"import.meta.url": "import_meta.url",
|
|
194
|
+
"import.meta.dirname": "import_meta.dirname",
|
|
195
|
+
"import.meta.filename": "import_meta.filename",
|
|
196
|
+
"import.meta": "import_meta",
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
return output.code;
|
|
200
|
+
} catch {
|
|
201
|
+
return source;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return source;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function requiresConversion(filePath: string, source: string): boolean {
|
|
209
|
+
// .mjs must stay ESM in the VFS -- tools read them directly. the runtime
|
|
210
|
+
// handles ESM->CJS on-the-fly for require().
|
|
211
|
+
if (filePath.endsWith(".mjs")) return false;
|
|
212
|
+
if (filePath.endsWith(".cjs")) return false;
|
|
213
|
+
return (
|
|
214
|
+
/\bimport\s+[\w{*'"]/m.test(source) ||
|
|
215
|
+
/\bexport\s+(?:default|const|let|var|function|class|{|\*)/m.test(source) ||
|
|
216
|
+
/\bimport\.meta\b/.test(source)
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function needsBuiltinPatching(source: string): boolean {
|
|
221
|
+
return (
|
|
222
|
+
/\bimport\s*\(\s*["']node:/.test(source) ||
|
|
223
|
+
/\bimport\s*\(\s*["'](fs|path|http|https|net|url|util|events|stream|os|crypto)["']/.test(
|
|
224
|
+
source,
|
|
225
|
+
)
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function patchBuiltinImports(source: string): string {
|
|
230
|
+
let patched = source;
|
|
231
|
+
patched = patched.replace(
|
|
232
|
+
/\bimport\s*\(\s*["']node:([^"']+)["']\s*\)/g,
|
|
233
|
+
'Promise.resolve(require("node:$1"))',
|
|
234
|
+
);
|
|
235
|
+
const builtins = [
|
|
236
|
+
"assert",
|
|
237
|
+
"buffer",
|
|
238
|
+
"child_process",
|
|
239
|
+
"cluster",
|
|
240
|
+
"crypto",
|
|
241
|
+
"dgram",
|
|
242
|
+
"dns",
|
|
243
|
+
"events",
|
|
244
|
+
"fs",
|
|
245
|
+
"http",
|
|
246
|
+
"http2",
|
|
247
|
+
"https",
|
|
248
|
+
"net",
|
|
249
|
+
"os",
|
|
250
|
+
"path",
|
|
251
|
+
"perf_hooks",
|
|
252
|
+
"querystring",
|
|
253
|
+
"readline",
|
|
254
|
+
"stream",
|
|
255
|
+
"string_decoder",
|
|
256
|
+
"timers",
|
|
257
|
+
"tls",
|
|
258
|
+
"url",
|
|
259
|
+
"util",
|
|
260
|
+
"v8",
|
|
261
|
+
"vm",
|
|
262
|
+
"worker_threads",
|
|
263
|
+
"zlib",
|
|
264
|
+
"async_hooks",
|
|
265
|
+
"inspector",
|
|
266
|
+
"module",
|
|
267
|
+
];
|
|
268
|
+
for (const b of builtins) {
|
|
269
|
+
patched = patched.replace(
|
|
270
|
+
new RegExp(`\\bimport\\s*\\(\\s*["']${b}["']\\s*\\)`, "g"),
|
|
271
|
+
`Promise.resolve(require("${b}"))`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return patched;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function listJsFiles(vol: MemoryVolume, dir: string): string[] {
|
|
278
|
+
const result: string[] = [];
|
|
279
|
+
try {
|
|
280
|
+
for (const name of vol.readdirSync(dir)) {
|
|
281
|
+
const full = dir + "/" + name;
|
|
282
|
+
try {
|
|
283
|
+
const info = vol.statSync(full);
|
|
284
|
+
if (info.isDirectory()) {
|
|
285
|
+
if (name !== "node_modules") result.push(...listJsFiles(vol, full));
|
|
286
|
+
} else if (
|
|
287
|
+
name.endsWith(".js") ||
|
|
288
|
+
name.endsWith(".mjs") ||
|
|
289
|
+
name.endsWith(".jsx")
|
|
290
|
+
) {
|
|
291
|
+
result.push(full);
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function isEsmPackage(vol: MemoryVolume, packageDir: string): boolean {
|
|
302
|
+
try {
|
|
303
|
+
const pkgPath = packageDir + "/package.json";
|
|
304
|
+
if (vol.existsSync(pkgPath)) {
|
|
305
|
+
const raw = vol.readFileSync(pkgPath, "utf8");
|
|
306
|
+
const pkg = JSON.parse(raw);
|
|
307
|
+
return pkg.type === "module";
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// convert all ESM files in a package directory to CJS
|
|
315
|
+
export async function convertPackage(
|
|
316
|
+
vol: MemoryVolume,
|
|
317
|
+
packageDir: string,
|
|
318
|
+
onProgress?: (msg: string) => void,
|
|
319
|
+
): Promise<number> {
|
|
320
|
+
let converted = 0;
|
|
321
|
+
const files = listJsFiles(vol, packageDir);
|
|
322
|
+
onProgress?.(` Converting ${files.length} files in ${packageDir}...`);
|
|
323
|
+
|
|
324
|
+
// "type":"module" packages stay as ESM in VFS -- tools like Vite read them
|
|
325
|
+
// directly and expect ESM syntax. require() handles conversion at runtime.
|
|
326
|
+
const esmPackage = isEsmPackage(vol, packageDir);
|
|
327
|
+
|
|
328
|
+
const BATCH = 50;
|
|
329
|
+
for (let i = 0; i < files.length; i += BATCH) {
|
|
330
|
+
const batch = files.slice(i, i + BATCH);
|
|
331
|
+
await Promise.all(
|
|
332
|
+
batch.map(async (fp) => {
|
|
333
|
+
try {
|
|
334
|
+
const src = vol.readFileSync(fp, "utf8");
|
|
335
|
+
|
|
336
|
+
if (esmPackage) {
|
|
337
|
+
if (needsBuiltinPatching(src)) {
|
|
338
|
+
vol.writeFileSync(fp, patchBuiltinImports(src));
|
|
339
|
+
converted++;
|
|
340
|
+
}
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// .mjs stays ESM even in non-ESM packages
|
|
345
|
+
if (fp.endsWith(".mjs")) {
|
|
346
|
+
if (needsBuiltinPatching(src)) {
|
|
347
|
+
vol.writeFileSync(fp, patchBuiltinImports(src));
|
|
348
|
+
converted++;
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (requiresConversion(fp, src)) {
|
|
354
|
+
const out = await convertFile(src, fp);
|
|
355
|
+
vol.writeFileSync(fp, out);
|
|
356
|
+
converted++;
|
|
357
|
+
} else if (needsBuiltinPatching(src)) {
|
|
358
|
+
vol.writeFileSync(fp, patchBuiltinImports(src));
|
|
359
|
+
converted++;
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
}),
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return converted;
|
|
368
|
+
}
|