@scelar/nodepod 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.
Files changed (134) hide show
  1. package/LICENSE +43 -0
  2. package/README.md +240 -0
  3. package/dist/child_process-BJOMsZje.js +8233 -0
  4. package/dist/child_process-BJOMsZje.js.map +1 -0
  5. package/dist/child_process-Cj8vOcuc.cjs +7434 -0
  6. package/dist/child_process-Cj8vOcuc.cjs.map +1 -0
  7. package/dist/index-Cb1Cgdnd.js +35308 -0
  8. package/dist/index-Cb1Cgdnd.js.map +1 -0
  9. package/dist/index-DsMGS-xc.cjs +37195 -0
  10. package/dist/index-DsMGS-xc.cjs.map +1 -0
  11. package/dist/index.cjs +65 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.mjs +59 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/package.json +95 -0
  16. package/src/__tests__/smoke.test.ts +11 -0
  17. package/src/constants/cdn-urls.ts +18 -0
  18. package/src/constants/config.ts +236 -0
  19. package/src/cross-origin.ts +26 -0
  20. package/src/engine-factory.ts +176 -0
  21. package/src/engine-types.ts +56 -0
  22. package/src/helpers/byte-encoding.ts +39 -0
  23. package/src/helpers/digest.ts +9 -0
  24. package/src/helpers/event-loop.ts +96 -0
  25. package/src/helpers/wasm-cache.ts +133 -0
  26. package/src/iframe-sandbox.ts +141 -0
  27. package/src/index.ts +192 -0
  28. package/src/isolation-helpers.ts +148 -0
  29. package/src/memory-volume.ts +941 -0
  30. package/src/module-transformer.ts +368 -0
  31. package/src/packages/archive-extractor.ts +248 -0
  32. package/src/packages/browser-bundler.ts +284 -0
  33. package/src/packages/installer.ts +396 -0
  34. package/src/packages/registry-client.ts +131 -0
  35. package/src/packages/version-resolver.ts +411 -0
  36. package/src/polyfills/assert.ts +384 -0
  37. package/src/polyfills/async_hooks.ts +144 -0
  38. package/src/polyfills/buffer.ts +628 -0
  39. package/src/polyfills/child_process.ts +2288 -0
  40. package/src/polyfills/chokidar.ts +336 -0
  41. package/src/polyfills/cluster.ts +106 -0
  42. package/src/polyfills/console.ts +136 -0
  43. package/src/polyfills/constants.ts +123 -0
  44. package/src/polyfills/crypto.ts +885 -0
  45. package/src/polyfills/dgram.ts +87 -0
  46. package/src/polyfills/diagnostics_channel.ts +76 -0
  47. package/src/polyfills/dns.ts +134 -0
  48. package/src/polyfills/domain.ts +68 -0
  49. package/src/polyfills/esbuild.ts +854 -0
  50. package/src/polyfills/events.ts +276 -0
  51. package/src/polyfills/fs.ts +2888 -0
  52. package/src/polyfills/fsevents.ts +79 -0
  53. package/src/polyfills/http.ts +1449 -0
  54. package/src/polyfills/http2.ts +199 -0
  55. package/src/polyfills/https.ts +76 -0
  56. package/src/polyfills/inspector.ts +62 -0
  57. package/src/polyfills/lightningcss.ts +105 -0
  58. package/src/polyfills/module.ts +191 -0
  59. package/src/polyfills/net.ts +353 -0
  60. package/src/polyfills/os.ts +238 -0
  61. package/src/polyfills/path.ts +206 -0
  62. package/src/polyfills/perf_hooks.ts +102 -0
  63. package/src/polyfills/process.ts +690 -0
  64. package/src/polyfills/punycode.ts +159 -0
  65. package/src/polyfills/querystring.ts +93 -0
  66. package/src/polyfills/quic.ts +118 -0
  67. package/src/polyfills/readdirp.ts +229 -0
  68. package/src/polyfills/readline.ts +692 -0
  69. package/src/polyfills/repl.ts +134 -0
  70. package/src/polyfills/rollup.ts +119 -0
  71. package/src/polyfills/sea.ts +33 -0
  72. package/src/polyfills/sqlite.ts +78 -0
  73. package/src/polyfills/stream.ts +1620 -0
  74. package/src/polyfills/string_decoder.ts +25 -0
  75. package/src/polyfills/tailwindcss-oxide.ts +309 -0
  76. package/src/polyfills/test.ts +197 -0
  77. package/src/polyfills/timers.ts +32 -0
  78. package/src/polyfills/tls.ts +105 -0
  79. package/src/polyfills/trace_events.ts +50 -0
  80. package/src/polyfills/tty.ts +71 -0
  81. package/src/polyfills/url.ts +174 -0
  82. package/src/polyfills/util.ts +559 -0
  83. package/src/polyfills/v8.ts +126 -0
  84. package/src/polyfills/vm.ts +132 -0
  85. package/src/polyfills/volume-registry.ts +15 -0
  86. package/src/polyfills/wasi.ts +44 -0
  87. package/src/polyfills/worker_threads.ts +326 -0
  88. package/src/polyfills/ws.ts +595 -0
  89. package/src/polyfills/zlib.ts +881 -0
  90. package/src/request-proxy.ts +716 -0
  91. package/src/script-engine.ts +3375 -0
  92. package/src/sdk/nodepod-fs.ts +93 -0
  93. package/src/sdk/nodepod-process.ts +86 -0
  94. package/src/sdk/nodepod-terminal.ts +350 -0
  95. package/src/sdk/nodepod.ts +509 -0
  96. package/src/sdk/types.ts +70 -0
  97. package/src/shell/commands/bun.ts +121 -0
  98. package/src/shell/commands/directory.ts +297 -0
  99. package/src/shell/commands/file-ops.ts +525 -0
  100. package/src/shell/commands/git.ts +2142 -0
  101. package/src/shell/commands/node.ts +80 -0
  102. package/src/shell/commands/npm.ts +198 -0
  103. package/src/shell/commands/pm-types.ts +45 -0
  104. package/src/shell/commands/pnpm.ts +82 -0
  105. package/src/shell/commands/search.ts +264 -0
  106. package/src/shell/commands/shell-env.ts +352 -0
  107. package/src/shell/commands/text-processing.ts +1152 -0
  108. package/src/shell/commands/yarn.ts +84 -0
  109. package/src/shell/shell-builtins.ts +19 -0
  110. package/src/shell/shell-helpers.ts +250 -0
  111. package/src/shell/shell-interpreter.ts +514 -0
  112. package/src/shell/shell-parser.ts +429 -0
  113. package/src/shell/shell-types.ts +85 -0
  114. package/src/syntax-transforms.ts +561 -0
  115. package/src/threading/engine-worker.ts +64 -0
  116. package/src/threading/inline-worker.ts +372 -0
  117. package/src/threading/offload-types.ts +112 -0
  118. package/src/threading/offload-worker.ts +383 -0
  119. package/src/threading/offload.ts +271 -0
  120. package/src/threading/process-context.ts +92 -0
  121. package/src/threading/process-handle.ts +275 -0
  122. package/src/threading/process-manager.ts +956 -0
  123. package/src/threading/process-worker-entry.ts +854 -0
  124. package/src/threading/shared-vfs.ts +352 -0
  125. package/src/threading/sync-channel.ts +135 -0
  126. package/src/threading/task-queue.ts +177 -0
  127. package/src/threading/vfs-bridge.ts +231 -0
  128. package/src/threading/worker-pool.ts +233 -0
  129. package/src/threading/worker-protocol.ts +358 -0
  130. package/src/threading/worker-vfs.ts +218 -0
  131. package/src/types/externals.d.ts +38 -0
  132. package/src/types/fs-streams.ts +142 -0
  133. package/src/types/manifest.ts +17 -0
  134. package/src/worker-sandbox.ts +90 -0
@@ -0,0 +1,854 @@
1
+ // Per-process Web Worker entry point.
2
+ // Each worker gets its own MemoryVolume, ScriptEngine, and NodepodShell.
3
+ // I/O flows via postMessage using the protocol in worker-protocol.ts.
4
+
5
+ import { MemoryVolume } from "../memory-volume";
6
+ import { ScriptEngine, setChildProcessPolyfill } from "../script-engine";
7
+ import { SyncChannelWorker } from "./sync-channel";
8
+ import type {
9
+ MainToWorkerMessage,
10
+ MainToWorker_Init,
11
+ MainToWorker_Exec,
12
+ WorkerToMainMessage,
13
+ } from "./worker-protocol";
14
+
15
+ // --- Worker state ---
16
+
17
+ let _pid = 0;
18
+ let _cwd = "/";
19
+ let _env: Record<string, string> = {};
20
+ let _volume: MemoryVolume | null = null;
21
+ let _initialized = false;
22
+ let _abortController: AbortController | null = null;
23
+
24
+ // Prevents echo loops: suppress vfs-write/delete while applying inbound vfs-sync
25
+ let _suppressVFSWatch = false;
26
+
27
+ let _shellInitialized = false;
28
+ let _shellMod: typeof import("../polyfills/child_process") | null = null;
29
+ let _syncChannelWorker: SyncChannelWorker | null = null;
30
+ let _ipcMessageHandler: ((data: unknown) => void) | null = null;
31
+ let _cols = 80;
32
+ let _rows = 24;
33
+
34
+ const _spawnCallbacks = new Map<number, (result: any) => void>();
35
+ const _childOutputCallbacks = new Map<number, (stream: string, data: string) => void>();
36
+ const _childExitCallbacks = new Map<number, (exitCode: number, stdout: string, stderr: string) => void>();
37
+ const _ipcCallbacks = new Map<number, (data: unknown) => void>();
38
+ let _nextRequestId = 1;
39
+
40
+ // --- Post to main thread ---
41
+
42
+ function post(msg: WorkerToMainMessage, transfer?: Transferable[]): void {
43
+ (self as unknown as Worker).postMessage(msg, transfer ?? []);
44
+ }
45
+
46
+ function postStdout(data: string): void {
47
+ post({ type: "stdout", data });
48
+ }
49
+
50
+ function postStderr(data: string): void {
51
+ post({ type: "stderr", data });
52
+ }
53
+
54
+ function postExit(exitCode: number, stdout: string, stderr: string): void {
55
+ post({ type: "exit", exitCode, stdout, stderr });
56
+ }
57
+
58
+ function postError(message: string, stack?: string): void {
59
+ post({ type: "error", message, stack });
60
+ }
61
+
62
+ function postCwdChange(cwd: string): void {
63
+ post({ type: "cwd-change", cwd });
64
+ }
65
+
66
+ function postStdinRawStatus(isRaw: boolean): void {
67
+ post({ type: "stdin-raw-status", isRaw });
68
+ }
69
+
70
+ // --- Message handler ---
71
+
72
+ self.addEventListener("message", (ev: MessageEvent) => {
73
+ if (!ev?.data?.type) return;
74
+ const msg = ev.data as MainToWorkerMessage;
75
+
76
+ switch (msg.type) {
77
+ case "init":
78
+ handleInit(msg);
79
+ break;
80
+ case "exec":
81
+ handleExec(msg);
82
+ break;
83
+ case "stdin":
84
+ handleStdin(msg.data);
85
+ break;
86
+ case "signal":
87
+ handleSignal(msg);
88
+ break;
89
+ case "resize":
90
+ _cols = msg.cols;
91
+ _rows = msg.rows;
92
+ break;
93
+ case "vfs-sync":
94
+ handleVFSSync(msg);
95
+ break;
96
+ case "vfs-chunk":
97
+ handleVFSChunk(msg);
98
+ break;
99
+ case "spawn-result":
100
+ _spawnCallbacks.get(msg.requestId)?.(msg);
101
+ _spawnCallbacks.delete(msg.requestId);
102
+ break;
103
+ case "child-output": {
104
+ const cb = _childOutputCallbacks.get(msg.requestId);
105
+ if (cb) cb(msg.stream, msg.data);
106
+ break;
107
+ }
108
+ case "child-exit": {
109
+ const cb = _childExitCallbacks.get(msg.requestId);
110
+ if (cb) {
111
+ _childExitCallbacks.delete(msg.requestId);
112
+ _childOutputCallbacks.delete(msg.requestId);
113
+ cb(msg.exitCode, msg.stdout, msg.stderr);
114
+ }
115
+ break;
116
+ }
117
+ case "ipc-message": {
118
+ const ipcMsg = msg as any;
119
+ if (ipcMsg.targetRequestId !== undefined) {
120
+ // This worker is a PARENT — route to the fork callback for a child
121
+ const ipcCb = _ipcCallbacks.get(ipcMsg.targetRequestId);
122
+ if (ipcCb) ipcCb(ipcMsg.data);
123
+ } else {
124
+ // This worker IS a forked child — emit on process object
125
+ handleIPCMessage(ipcMsg.data);
126
+ }
127
+ break;
128
+ }
129
+ case "http-request":
130
+ handleHttpRequest(msg as any);
131
+ break;
132
+ case "ws-upgrade":
133
+ handleWsUpgrade(msg as any);
134
+ break;
135
+ case "ws-data":
136
+ handleWsData(msg as any);
137
+ break;
138
+ case "ws-close":
139
+ handleWsClose(msg as any);
140
+ break;
141
+ default:
142
+ break;
143
+ }
144
+ });
145
+
146
+ // --- Init ---
147
+
148
+ function handleInit(msg: MainToWorker_Init): void {
149
+ _pid = msg.pid;
150
+ _cwd = msg.cwd || "/";
151
+ _env = msg.env || {};
152
+
153
+ _volume = MemoryVolume.fromBinarySnapshot(msg.snapshot);
154
+
155
+ // Watch local writes → forward to main. Suppressed during inbound vfs-sync to prevent echo.
156
+ _volume.watch("/", { recursive: true }, (event, filename) => {
157
+ if (!filename || _suppressVFSWatch) return;
158
+ try {
159
+ if (_volume!.existsSync(filename)) {
160
+ const stat = _volume!.statSync(filename);
161
+ if (stat.isDirectory()) {
162
+ post({
163
+ type: "vfs-write",
164
+ path: filename,
165
+ content: new ArrayBuffer(0),
166
+ isDirectory: true,
167
+ });
168
+ } else {
169
+ const data = _volume!.readFileSync(filename);
170
+ const buffer = (data.buffer as ArrayBuffer).slice(
171
+ data.byteOffset,
172
+ data.byteOffset + data.byteLength,
173
+ );
174
+ post({ type: "vfs-write", path: filename, content: buffer, isDirectory: false }, [buffer]);
175
+ }
176
+ } else {
177
+ post({ type: "vfs-delete", path: filename });
178
+ }
179
+ } catch {
180
+ /* ignore */
181
+ }
182
+ });
183
+
184
+ if (msg.syncBuffer) {
185
+ _syncChannelWorker = new SyncChannelWorker(msg.syncBuffer);
186
+ }
187
+
188
+ _initialized = true;
189
+ post({ type: "ready", pid: _pid });
190
+ }
191
+
192
+ // --- Shell init (lazy) ---
193
+
194
+ async function ensureShell(): Promise<typeof import("../polyfills/child_process")> {
195
+ if (!_shellMod) {
196
+ _shellMod = await import("../polyfills/child_process");
197
+ // Must be eager — sync require('child_process') needs this before any microtask fires
198
+ setChildProcessPolyfill(_shellMod);
199
+ }
200
+ if (!_shellInitialized && _volume) {
201
+ _shellMod.initShellExec(_volume, { cwd: _cwd, env: _env });
202
+ if (_syncChannelWorker) {
203
+ _shellMod.setSyncChannel(_syncChannelWorker);
204
+ }
205
+ _shellMod.setSpawnChildCallback(spawnChild);
206
+ _shellMod.setForkChildCallback(forkChild);
207
+ const wtMod = await import("../polyfills/worker_threads");
208
+ wtMod.setWorkerThreadForkCallback(workerThreadFork);
209
+ const httpMod = await import("../polyfills/http");
210
+ httpMod.setServerListenCallback((port: number) => {
211
+ post({ type: "server-listen", port, hostname: "0.0.0.0" });
212
+ });
213
+ httpMod.setServerCloseCallback((port: number) => {
214
+ post({ type: "server-close", port });
215
+ });
216
+ _shellInitialized = true;
217
+ }
218
+ return _shellMod;
219
+ }
220
+
221
+ // --- Exec ---
222
+
223
+ async function handleExec(msg: MainToWorker_Exec): Promise<void> {
224
+ if (!_volume) {
225
+ postStderr("Error: VFS not initialized\n");
226
+ postExit(1, "", "Error: VFS not initialized\n");
227
+ return;
228
+ }
229
+
230
+ _abortController = new AbortController();
231
+
232
+ if (msg.cwd) {
233
+ _cwd = msg.cwd;
234
+ // Must sync shell cwd too, otherwise it keeps its old cwd
235
+ if (_shellMod) _shellMod.setShellCwd(msg.cwd);
236
+ }
237
+ if (msg.env) Object.assign(_env, msg.env);
238
+
239
+ if (msg.isShell) {
240
+ await handleShellExec(msg);
241
+ } else {
242
+ await handleFileExec(msg);
243
+ }
244
+ }
245
+
246
+ // --- Shell command execution ---
247
+
248
+ async function handleShellExec(msg: MainToWorker_Exec): Promise<void> {
249
+ const shellCmd = msg.shellCommand || "";
250
+ const persistent = !!msg.persistent;
251
+
252
+ try {
253
+ const shell = await ensureShell();
254
+
255
+ shell.setStreamingCallbacks({
256
+ onStdout: postStdout,
257
+ onStderr: postStderr,
258
+ signal: _abortController!.signal,
259
+ getCols: () => _cols,
260
+ getRows: () => _rows,
261
+ onRawModeChange: postStdinRawStatus,
262
+ });
263
+
264
+ // shellExec() NOT child_process.exec() — the latter spawns a new worker, causing recursion
265
+ shell.shellExec(shellCmd, {}, (error, stdout, stderr) => {
266
+ // Don't clearStreamingCallbacks — background children (e.g. vite dev)
267
+ // may still need the output sinks. Cleaned up on worker termination.
268
+
269
+ const newCwd = shell.getShellCwd();
270
+ if (newCwd !== _cwd) {
271
+ _cwd = newCwd;
272
+ postCwdChange(newCwd);
273
+ }
274
+
275
+ postStdinRawStatus(false);
276
+
277
+ const exitCode = error ? ((error as any).code ?? 1) : 0;
278
+ const outStr = String(stdout ?? "");
279
+ const errStr = String(stderr ?? "");
280
+
281
+ if (persistent) {
282
+ post({ type: "shell-done", exitCode, stdout: outStr, stderr: errStr });
283
+ } else {
284
+ postExit(exitCode, outStr, errStr);
285
+ }
286
+ });
287
+ } catch (e: any) {
288
+ postError(e?.message || String(e), e?.stack);
289
+ if (persistent) {
290
+ post({ type: "shell-done", exitCode: 1, stdout: "", stderr: e?.message || String(e) });
291
+ } else {
292
+ postExit(1, "", e?.message || String(e));
293
+ }
294
+ }
295
+ }
296
+
297
+ // --- IPC message handler ---
298
+
299
+ function handleIPCMessage(data: unknown): void {
300
+ if (_shellMod) {
301
+ _shellMod.handleIPCFromParent(data);
302
+ } else if (_ipcMessageHandler) {
303
+ _ipcMessageHandler(data);
304
+ }
305
+ }
306
+
307
+ // --- File execution (node script.js) ---
308
+
309
+ async function handleFileExec(msg: MainToWorker_Exec): Promise<void> {
310
+ const filePath = msg.filePath;
311
+ const args = msg.args || [];
312
+
313
+ try {
314
+ const shell = await ensureShell();
315
+
316
+ // Enable IPC for forks: process.send() → postMessage to main
317
+ if (msg.isFork) {
318
+ shell.setIPCSend((data: unknown) => {
319
+ post({ type: "ipc-message", data });
320
+ });
321
+ }
322
+
323
+ let workerThreadsOverride: {
324
+ isMainThread: boolean;
325
+ parentPort: unknown;
326
+ workerData: unknown;
327
+ threadId: number;
328
+ } | undefined;
329
+
330
+ if (msg.isWorkerThread) {
331
+ const { MessagePort } = await import("../polyfills/worker_threads");
332
+ const pp = new MessagePort();
333
+ pp.postMessage = (data: unknown) => {
334
+ post({ type: "ipc-message", data });
335
+ };
336
+
337
+ shell.setIPCReceiveHandler((data: unknown) => {
338
+ pp.emit("message", data);
339
+ });
340
+
341
+ workerThreadsOverride = {
342
+ isMainThread: false,
343
+ parentPort: pp,
344
+ workerData: msg.workerData ?? null,
345
+ threadId: msg.threadId ?? 0,
346
+ };
347
+ }
348
+
349
+ shell.setStreamingCallbacks({
350
+ onStdout: postStdout,
351
+ onStderr: postStderr,
352
+ signal: _abortController!.signal,
353
+ getCols: () => _cols,
354
+ getRows: () => _rows,
355
+ onRawModeChange: postStdinRawStatus,
356
+ });
357
+
358
+ const ctx = {
359
+ cwd: _cwd,
360
+ env: _env,
361
+ volume: _volume!,
362
+ exec: async (cmd: string, opts?: { cwd?: string; env?: Record<string, string> }) => {
363
+ // Needed for ShellContext type compat
364
+ return new Promise<{ stdout: string; stderr: string; exitCode: number }>((resolve) => {
365
+ shell.exec(cmd, opts ?? {}, (error: any, stdout: any, stderr: any) => {
366
+ resolve({
367
+ stdout: String(stdout ?? ""),
368
+ stderr: String(stderr ?? ""),
369
+ exitCode: error ? (error.code ?? 1) : 0,
370
+ });
371
+ });
372
+ });
373
+ },
374
+ };
375
+
376
+ const result = await shell.executeNodeBinary(filePath, args, ctx, {
377
+ isFork: !!msg.isFork,
378
+ workerThreadsOverride,
379
+ });
380
+
381
+ shell.clearStreamingCallbacks();
382
+ postExit(result.exitCode, result.stdout, result.stderr);
383
+ } catch (e: any) {
384
+ // Safety net — executeNodeBinary handles its own errors, but just in case:
385
+ const errMsg = e?.message || String(e);
386
+ postStderr(`Error: ${errMsg}\n`);
387
+ postExit(1, "", errMsg);
388
+ }
389
+ }
390
+
391
+ // --- Stdin ---
392
+
393
+ function handleStdin(data: string): void {
394
+ if (!_shellMod) return;
395
+ try {
396
+ _shellMod.sendStdin(data);
397
+ } catch {
398
+ /* ignore */
399
+ }
400
+ }
401
+
402
+ // --- Signal ---
403
+
404
+ function handleSignal(msg: { signal: string }): void {
405
+ if (msg.signal === "SIGINT" || msg.signal === "SIGTERM") {
406
+ if (_abortController) {
407
+ _abortController.abort();
408
+ }
409
+ }
410
+ }
411
+
412
+ // --- VFS sync ---
413
+
414
+ function handleVFSSync(msg: { path: string; content: ArrayBuffer | null; isDirectory: boolean }): void {
415
+ if (!_volume) return;
416
+ // Suppress watcher — these came from another worker, don't echo back
417
+ _suppressVFSWatch = true;
418
+ try {
419
+ if (msg.content === null) {
420
+ if (_volume.existsSync(msg.path)) {
421
+ const stat = _volume.statSync(msg.path);
422
+ if (stat.isDirectory()) {
423
+ _volume.rmdirSync(msg.path);
424
+ } else {
425
+ _volume.unlinkSync(msg.path);
426
+ }
427
+ }
428
+ } else if (msg.isDirectory) {
429
+ if (!_volume.existsSync(msg.path)) {
430
+ _volume.mkdirSync(msg.path, { recursive: true });
431
+ }
432
+ } else {
433
+ const parentDir = msg.path.substring(0, msg.path.lastIndexOf("/")) || "/";
434
+ if (parentDir !== "/" && !_volume.existsSync(parentDir)) {
435
+ _volume.mkdirSync(parentDir, { recursive: true });
436
+ }
437
+ _volume.writeFileSync(msg.path, new Uint8Array(msg.content));
438
+ }
439
+ } catch {
440
+ /* ignore */
441
+ } finally {
442
+ _suppressVFSWatch = false;
443
+ }
444
+ }
445
+
446
+ let _vfsChunks: Array<{ data: ArrayBuffer; manifest: Array<{ path: string; offset: number; length: number; isDirectory: boolean }> } | null> = [];
447
+
448
+ function handleVFSChunk(msg: { chunkIndex: number; totalChunks: number; data: ArrayBuffer; manifest: Array<{ path: string; offset: number; length: number; isDirectory: boolean }> }): void {
449
+ _vfsChunks[msg.chunkIndex] = { data: msg.data, manifest: msg.manifest };
450
+ const received = _vfsChunks.filter(Boolean).length;
451
+ if (received === msg.totalChunks) {
452
+ // All chunks received — apply to volume
453
+ _suppressVFSWatch = true;
454
+ try {
455
+ if (_volume) {
456
+ for (const chunk of _vfsChunks) {
457
+ if (!chunk) continue;
458
+ const data = new Uint8Array(chunk.data);
459
+ for (const entry of chunk.manifest) {
460
+ if (entry.isDirectory) {
461
+ if (!_volume.existsSync(entry.path)) {
462
+ _volume.mkdirSync(entry.path, { recursive: true });
463
+ }
464
+ } else {
465
+ const parentDir = entry.path.substring(0, entry.path.lastIndexOf("/")) || "/";
466
+ if (parentDir !== "/" && !_volume.existsSync(parentDir)) {
467
+ _volume.mkdirSync(parentDir, { recursive: true });
468
+ }
469
+ _volume.writeFileSync(entry.path, data.slice(entry.offset, entry.offset + entry.length));
470
+ }
471
+ }
472
+ }
473
+ }
474
+ } finally {
475
+ _suppressVFSWatch = false;
476
+ }
477
+ _vfsChunks = [];
478
+ }
479
+ }
480
+
481
+ // --- HTTP request dispatch ---
482
+
483
+ async function handleHttpRequest(msg: {
484
+ requestId: number;
485
+ port: number;
486
+ method: string;
487
+ path: string;
488
+ headers: Record<string, string>;
489
+ body: string | null;
490
+ }): Promise<void> {
491
+ try {
492
+ const httpMod = await import("../polyfills/http");
493
+ const server = httpMod.getServer(msg.port);
494
+ if (!server) {
495
+ post({
496
+ type: "http-response",
497
+ requestId: msg.requestId,
498
+ statusCode: 503,
499
+ statusMessage: "Service Unavailable",
500
+ headers: { "Content-Type": "text/plain" },
501
+ body: `No server on port ${msg.port}`,
502
+ } as any);
503
+ return;
504
+ }
505
+
506
+ const { Buffer } = await import("../polyfills/buffer");
507
+ const bodyBuf = msg.body ? Buffer.from(msg.body) : undefined;
508
+ const result = await server.dispatchRequest(
509
+ msg.method,
510
+ msg.path,
511
+ msg.headers,
512
+ bodyBuf,
513
+ );
514
+ // Can't transfer Buffer via postMessage, convert to string
515
+ let bodyStr = "";
516
+ if (result.body) {
517
+ if (typeof result.body === "string") {
518
+ bodyStr = result.body;
519
+ } else if (result.body instanceof Uint8Array || Buffer.isBuffer(result.body)) {
520
+ bodyStr = new TextDecoder().decode(result.body);
521
+ } else {
522
+ bodyStr = String(result.body);
523
+ }
524
+ }
525
+
526
+ post({
527
+ type: "http-response",
528
+ requestId: msg.requestId,
529
+ statusCode: result.statusCode,
530
+ statusMessage: result.statusMessage,
531
+ headers: result.headers,
532
+ body: bodyStr,
533
+ } as any);
534
+ } catch (e: any) {
535
+ console.error(`[DEBUG] handleHttpRequest caught:`, e?.message, "\nStack:", e?.stack?.split?.("\n")?.slice(0, 8)?.join("\n"));
536
+ post({
537
+ type: "http-response",
538
+ requestId: msg.requestId,
539
+ statusCode: 500,
540
+ statusMessage: "Internal Server Error",
541
+ headers: { "Content-Type": "text/plain" },
542
+ body: e?.message || "Internal Server Error",
543
+ } as any);
544
+ }
545
+ }
546
+
547
+ // --- WebSocket upgrade handling ---
548
+
549
+ // Active WS connections: uid → socket (TcpSocket in the worker)
550
+ const _wsConnections = new Map<string, any>();
551
+
552
+ async function handleWsUpgrade(msg: {
553
+ uid: string;
554
+ port: number;
555
+ path: string;
556
+ headers: Record<string, string>;
557
+ }): Promise<void> {
558
+ try {
559
+ const httpMod = await import("../polyfills/http");
560
+ const { encodeFrame, decodeFrame } = httpMod;
561
+ const { Buffer } = await import("../polyfills/buffer");
562
+
563
+ const server = httpMod.getServer(msg.port);
564
+ if (!server) {
565
+ post({ type: "ws-frame", uid: msg.uid, kind: "error", message: `No server on port ${msg.port}` } as any);
566
+ return;
567
+ }
568
+
569
+ const { socket } = server.dispatchUpgrade(msg.path || "/", msg.headers);
570
+
571
+ let outboundBuf = new Uint8Array(0);
572
+ let handshakeDone = false;
573
+
574
+ // Intercept socket.write to decode WS frames and relay to main thread
575
+ socket.write = ((
576
+ chunk: Uint8Array | string,
577
+ encOrCb?: string | ((err?: Error | null) => void),
578
+ cb?: (err?: Error | null) => void,
579
+ ): boolean => {
580
+ const raw = typeof chunk === "string" ? Buffer.from(chunk) : new Uint8Array(chunk);
581
+ const fn = typeof encOrCb === "function" ? encOrCb : cb;
582
+
583
+ if (!handshakeDone) {
584
+ const text = new TextDecoder().decode(raw);
585
+ if (text.startsWith("HTTP/1.1 101")) {
586
+ handshakeDone = true;
587
+ post({ type: "ws-frame", uid: msg.uid, kind: "open" } as any);
588
+ if (fn) queueMicrotask(() => fn(null));
589
+ return true;
590
+ }
591
+ }
592
+
593
+ const merged = new Uint8Array(outboundBuf.length + raw.length);
594
+ merged.set(outboundBuf, 0);
595
+ merged.set(raw, outboundBuf.length);
596
+ outboundBuf = merged;
597
+
598
+ while (outboundBuf.length >= 2) {
599
+ const frame = decodeFrame(outboundBuf);
600
+ if (!frame) break;
601
+ outboundBuf = outboundBuf.slice(frame.consumed);
602
+
603
+ switch (frame.op) {
604
+ case 0x01: { // TEXT
605
+ const text = new TextDecoder().decode(frame.data);
606
+ post({ type: "ws-frame", uid: msg.uid, kind: "text", data: text } as any);
607
+ break;
608
+ }
609
+ case 0x02: // BINARY
610
+ post({ type: "ws-frame", uid: msg.uid, kind: "binary", bytes: Array.from(frame.data) } as any);
611
+ break;
612
+ case 0x08: { // CLOSE
613
+ const code = frame.data.length >= 2 ? (frame.data[0] << 8) | frame.data[1] : 1000;
614
+ post({ type: "ws-frame", uid: msg.uid, kind: "close", code } as any);
615
+ _wsConnections.delete(msg.uid);
616
+ break;
617
+ }
618
+ case 0x09: // PING — send pong back
619
+ socket._feedData(Buffer.from(encodeFrame(0x0A, frame.data, true)));
620
+ break;
621
+ }
622
+ }
623
+
624
+ if (fn) queueMicrotask(() => fn(null));
625
+ return true;
626
+ }) as any;
627
+
628
+ _wsConnections.set(msg.uid, socket);
629
+ } catch (e: any) {
630
+ post({ type: "ws-frame", uid: msg.uid, kind: "error", message: e?.message || "WS upgrade failed" } as any);
631
+ }
632
+ }
633
+
634
+ function handleWsData(msg: { uid: string; frame: number[] }): void {
635
+ const socket = _wsConnections.get(msg.uid);
636
+ if (!socket) return;
637
+ try {
638
+ const { Buffer } = require("../polyfills/buffer");
639
+ socket._feedData(Buffer.from(new Uint8Array(msg.frame)));
640
+ } catch { /* ignore */ }
641
+ }
642
+
643
+ function handleWsClose(msg: { uid: string; code: number }): void {
644
+ const socket = _wsConnections.get(msg.uid);
645
+ if (!socket) return;
646
+ try {
647
+ const codeBuf = new Uint8Array(2);
648
+ codeBuf[0] = (msg.code >> 8) & 0xff;
649
+ codeBuf[1] = msg.code & 0xff;
650
+ const { encodeFrame } = require("../polyfills/http");
651
+ const { Buffer } = require("../polyfills/buffer");
652
+ socket._feedData(Buffer.from(encodeFrame(0x08, codeBuf, true)));
653
+ } catch { /* ignore */ }
654
+ try { socket.destroy(); } catch { /* ignore */ }
655
+ _wsConnections.delete(msg.uid);
656
+ }
657
+
658
+ // --- Child process spawning ---
659
+
660
+ // Forks a child process via main thread. Returns IPC handles immediately;
661
+ // output and exit arrive via callbacks.
662
+ export function forkChild(
663
+ modulePath: string,
664
+ args: string[],
665
+ opts: {
666
+ cwd: string;
667
+ env: Record<string, string>;
668
+ onStdout?: (data: string) => void;
669
+ onStderr?: (data: string) => void;
670
+ onIPC?: (data: unknown) => void;
671
+ onExit?: (exitCode: number) => void;
672
+ },
673
+ ): { sendIPC: (data: unknown) => void; disconnect: () => void; requestId: number } {
674
+ const requestId = _nextRequestId++;
675
+
676
+ if (opts.onIPC) {
677
+ _ipcCallbacks.set(requestId, opts.onIPC);
678
+ }
679
+
680
+ _spawnCallbacks.set(requestId, (result: any) => {
681
+ if (result.error) {
682
+ _ipcCallbacks.delete(requestId);
683
+ opts.onStderr?.(`Fork error: ${result.error}\n`);
684
+ opts.onExit?.(1);
685
+ return;
686
+ }
687
+
688
+ _childOutputCallbacks.set(requestId, (stream: string, data: string) => {
689
+ if (stream === "stdout") {
690
+ opts.onStdout?.(data);
691
+ } else {
692
+ opts.onStderr?.(data);
693
+ }
694
+ });
695
+
696
+ _childExitCallbacks.set(requestId, (exitCode: number) => {
697
+ _ipcCallbacks.delete(requestId);
698
+ opts.onExit?.(exitCode);
699
+ });
700
+ });
701
+
702
+ post({
703
+ type: "fork-request",
704
+ requestId,
705
+ modulePath,
706
+ args,
707
+ cwd: opts.cwd,
708
+ env: opts.env,
709
+ });
710
+
711
+ return {
712
+ requestId,
713
+ sendIPC: (data: unknown) => {
714
+ post({
715
+ type: "ipc-message",
716
+ targetRequestId: requestId,
717
+ data,
718
+ });
719
+ },
720
+ disconnect: () => {
721
+ _ipcCallbacks.delete(requestId);
722
+ _childOutputCallbacks.delete(requestId);
723
+ _childExitCallbacks.delete(requestId);
724
+ },
725
+ };
726
+ }
727
+
728
+ // Same as forkChild but for worker_threads — posts "workerthread-request" with workerData/threadId
729
+ function workerThreadFork(
730
+ modulePath: string,
731
+ opts: {
732
+ workerData: unknown;
733
+ threadId: number;
734
+ isEval?: boolean;
735
+ cwd: string;
736
+ env: Record<string, string>;
737
+ onMessage: (data: unknown) => void;
738
+ onError: (err: Error) => void;
739
+ onExit: (code: number) => void;
740
+ onStdout?: (data: string) => void;
741
+ onStderr?: (data: string) => void;
742
+ },
743
+ ): { postMessage: (data: unknown) => void; terminate: () => void; requestId: number } {
744
+ const requestId = _nextRequestId++;
745
+
746
+ _ipcCallbacks.set(requestId, (data: unknown) => {
747
+ opts.onMessage(data);
748
+ });
749
+
750
+ _spawnCallbacks.set(requestId, (result: any) => {
751
+ if (result.error) {
752
+ _ipcCallbacks.delete(requestId);
753
+ opts.onError(new Error(result.error));
754
+ return;
755
+ }
756
+
757
+ _childOutputCallbacks.set(requestId, (stream: string, data: string) => {
758
+ if (stream === "stdout") {
759
+ opts.onStdout?.(data);
760
+ } else {
761
+ opts.onStderr?.(data);
762
+ }
763
+ });
764
+
765
+ _childExitCallbacks.set(requestId, (exitCode: number) => {
766
+ _ipcCallbacks.delete(requestId);
767
+ opts.onExit(exitCode);
768
+ });
769
+ });
770
+
771
+ post({
772
+ type: "workerthread-request",
773
+ requestId,
774
+ modulePath,
775
+ isEval: opts.isEval,
776
+ args: [],
777
+ cwd: opts.cwd,
778
+ env: opts.env,
779
+ workerData: opts.workerData,
780
+ threadId: opts.threadId,
781
+ } as any);
782
+
783
+ return {
784
+ requestId,
785
+ postMessage: (data: unknown) => {
786
+ post({
787
+ type: "ipc-message",
788
+ targetRequestId: requestId,
789
+ data,
790
+ });
791
+ },
792
+ terminate: () => {
793
+ _ipcCallbacks.delete(requestId);
794
+ _childOutputCallbacks.delete(requestId);
795
+ _childExitCallbacks.delete(requestId);
796
+ },
797
+ };
798
+ }
799
+
800
+ // Spawns a child process via main thread. Resolves when child exits.
801
+ // onStdout/onStderr fire in real-time as child-output messages arrive.
802
+ export function spawnChild(
803
+ command: string,
804
+ args: string[],
805
+ opts?: {
806
+ cwd?: string;
807
+ env?: Record<string, string>;
808
+ stdio?: "pipe" | "inherit";
809
+ onStdout?: (data: string) => void;
810
+ onStderr?: (data: string) => void;
811
+ },
812
+ ): Promise<{ pid: number; exitCode: number; stdout: string; stderr: string }> {
813
+ return new Promise((resolve, reject) => {
814
+ const requestId = _nextRequestId++;
815
+ let stdout = "";
816
+ let stderr = "";
817
+
818
+ _spawnCallbacks.set(requestId, (result: any) => {
819
+ if (result.error) {
820
+ reject(new Error(result.error));
821
+ return;
822
+ }
823
+
824
+ _childOutputCallbacks.set(requestId, (stream: string, data: string) => {
825
+ if (stream === "stdout") {
826
+ stdout += data;
827
+ opts?.onStdout?.(data);
828
+ } else {
829
+ stderr += data;
830
+ opts?.onStderr?.(data);
831
+ }
832
+ });
833
+
834
+ _childExitCallbacks.set(requestId, (exitCode: number, fullStdout: string, fullStderr: string) => {
835
+ resolve({
836
+ pid: result.pid,
837
+ exitCode,
838
+ stdout: fullStdout || stdout,
839
+ stderr: fullStderr || stderr,
840
+ });
841
+ });
842
+ });
843
+
844
+ post({
845
+ type: "spawn-request",
846
+ requestId,
847
+ command,
848
+ args,
849
+ cwd: opts?.cwd || _cwd,
850
+ env: opts?.env || _env,
851
+ stdio: opts?.stdio || "pipe",
852
+ });
853
+ });
854
+ }