@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,509 @@
1
+ import { MemoryVolume } from "../memory-volume";
2
+ import { ScriptEngine } from "../script-engine";
3
+ import { DependencyInstaller } from "../packages/installer";
4
+ import { RequestProxy, getProxyInstance, type IVirtualServer } from "../request-proxy";
5
+ import type { VolumeSnapshot } from "../engine-types";
6
+ import { Buffer } from "../polyfills/buffer";
7
+ import type {
8
+ NodepodOptions,
9
+ TerminalOptions,
10
+ Snapshot,
11
+ SpawnOptions,
12
+ } from "./types";
13
+ import { NodepodFS } from "./nodepod-fs";
14
+ import { NodepodProcess } from "./nodepod-process";
15
+ import { NodepodTerminal } from "./nodepod-terminal";
16
+ import { ProcessManager } from "../threading/process-manager";
17
+ import type { ProcessHandle } from "../threading/process-handle";
18
+ import { VFSBridge } from "../threading/vfs-bridge";
19
+ import { isSharedArrayBufferAvailable, SharedVFSController } from "../threading/shared-vfs";
20
+ import { SyncChannelController } from "../threading/sync-channel";
21
+
22
+ // Lazy-load child_process so the shell doesn't get pulled in at import time
23
+ let _shellMod: typeof import("../polyfills/child_process") | null = null;
24
+ async function getShellMod() {
25
+ if (!_shellMod) _shellMod = await import("../polyfills/child_process");
26
+ return _shellMod;
27
+ }
28
+
29
+ export class Nodepod {
30
+ readonly fs: NodepodFS;
31
+
32
+ private _volume: MemoryVolume;
33
+ private _engine: ScriptEngine;
34
+ private _packages: DependencyInstaller;
35
+ private _proxy: RequestProxy;
36
+ private _cwd: string;
37
+
38
+ private _processManager: ProcessManager;
39
+ private _vfsBridge: VFSBridge;
40
+ private _sharedVFS: SharedVFSController | null = null;
41
+ private _syncChannel: SyncChannelController | null = null;
42
+ private _unwatchVFS: (() => void) | null = null;
43
+
44
+ /* ---- Construction (use Nodepod.boot()) ---- */
45
+
46
+ private constructor(
47
+ volume: MemoryVolume,
48
+ engine: ScriptEngine,
49
+ packages: DependencyInstaller,
50
+ proxy: RequestProxy,
51
+ cwd: string,
52
+ ) {
53
+ this._volume = volume;
54
+ this._engine = engine;
55
+ this._packages = packages;
56
+ this._proxy = proxy;
57
+ this._cwd = cwd;
58
+ this.fs = new NodepodFS(volume);
59
+ this._processManager = new ProcessManager(volume);
60
+ this._vfsBridge = new VFSBridge(volume);
61
+
62
+ this._vfsBridge.setBroadcaster((path, content, excludePid) => {
63
+ const isDirectory = content !== null && content.byteLength === 0;
64
+ this._processManager.broadcastVFSChange(path, content, isDirectory, excludePid);
65
+ });
66
+
67
+ this._processManager.setVFSBridge(this._vfsBridge);
68
+
69
+ // VFS watcher broadcasts main-thread file changes to workers (needed for HMR)
70
+ this._unwatchVFS = this._vfsBridge.watch();
71
+
72
+ if (isSharedArrayBufferAvailable()) {
73
+ try {
74
+ this._sharedVFS = new SharedVFSController();
75
+ this._processManager.setSharedBuffer(this._sharedVFS.buffer);
76
+ this._vfsBridge.setSharedVFS(this._sharedVFS);
77
+ } catch (e) {
78
+ // COOP/COEP headers probably missing
79
+ }
80
+
81
+ try {
82
+ this._syncChannel = new SyncChannelController();
83
+ this._processManager.setSyncBuffer(this._syncChannel.buffer);
84
+ } catch (e) {
85
+ // SyncChannel init failed
86
+ }
87
+ }
88
+
89
+ // Bridge worker HTTP servers to the RequestProxy for preview URLs
90
+ this._processManager.on("server-listen", (_pid: number, port: number, _hostname: string) => {
91
+ const proxyServer: IVirtualServer = {
92
+ listening: true,
93
+ address: () => ({ port, address: "0.0.0.0", family: "IPv4" }),
94
+ dispatchRequest: async (method, url, headers, body) => {
95
+ const bodyStr = body ? (typeof body === "string" ? body : body.toString("utf8")) : null;
96
+ const result = await this._processManager.dispatchHttpRequest(
97
+ port, method, url, headers, bodyStr,
98
+ );
99
+ return {
100
+ statusCode: result.statusCode,
101
+ statusMessage: result.statusMessage,
102
+ headers: result.headers,
103
+ body: Buffer.from(result.body),
104
+ };
105
+ },
106
+ };
107
+ this._proxy.register(proxyServer, port);
108
+ });
109
+
110
+ this._processManager.on("server-close", (_pid: number, port: number) => {
111
+ this._proxy.unregister(port);
112
+ });
113
+
114
+ this._proxy.setProcessManager(this._processManager);
115
+ }
116
+
117
+ /* ---- Static factory ---- */
118
+
119
+ static async boot(opts: NodepodOptions = {}): Promise<Nodepod> {
120
+ if (typeof Worker === "undefined") {
121
+ throw new Error("[Nodepod] Web Workers are required. Nodepod cannot run without Web Worker support.");
122
+ }
123
+ if (typeof SharedArrayBuffer === "undefined") {
124
+ throw new Error("[Nodepod] SharedArrayBuffer is required. Ensure Cross-Origin-Isolation headers are set (Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: credentialless).");
125
+ }
126
+
127
+ const cwd = opts.workdir ?? "/";
128
+ const volume = new MemoryVolume();
129
+ const engine = new ScriptEngine(volume, {
130
+ cwd,
131
+ env: opts.env,
132
+ });
133
+ const packages = new DependencyInstaller(volume);
134
+ const proxy = getProxyInstance({
135
+ onServerReady: opts.onServerReady,
136
+ });
137
+
138
+ const nodepod = new Nodepod(volume, engine, packages, proxy, cwd);
139
+
140
+ if (opts.files) {
141
+ for (const [path, content] of Object.entries(opts.files)) {
142
+ const dir = path.substring(0, path.lastIndexOf("/")) || "/";
143
+ if (dir !== "/" && !volume.existsSync(dir)) {
144
+ volume.mkdirSync(dir, { recursive: true });
145
+ }
146
+ volume.writeFileSync(path, content as any);
147
+ }
148
+ }
149
+
150
+ if (cwd !== "/" && !volume.existsSync(cwd)) {
151
+ volume.mkdirSync(cwd, { recursive: true });
152
+ }
153
+
154
+ for (const dir of ["/tmp", "/home"]) {
155
+ if (!volume.existsSync(dir)) {
156
+ volume.mkdirSync(dir, { recursive: true });
157
+ }
158
+ }
159
+
160
+ const shell = await getShellMod();
161
+ shell.initShellExec(volume, { cwd, env: opts.env });
162
+
163
+ if (
164
+ opts.swUrl &&
165
+ typeof navigator !== "undefined" &&
166
+ "serviceWorker" in navigator
167
+ ) {
168
+ try {
169
+ await proxy.initServiceWorker({ swUrl: opts.swUrl });
170
+ // Watermark is on by default — only disable if explicitly set to false
171
+ if (opts.watermark === false) {
172
+ proxy.setWatermark(false);
173
+ }
174
+ } catch (e) {
175
+ // SW registration failed, non-fatal
176
+ }
177
+ }
178
+
179
+ return nodepod;
180
+ }
181
+
182
+ /* ---- spawn() ---- */
183
+
184
+ // Each spawn gets a dedicated worker with its own engine + shell
185
+ async spawn(
186
+ cmd: string,
187
+ args?: string[],
188
+ opts?: SpawnOptions,
189
+ ): Promise<NodepodProcess> {
190
+ const proc = new NodepodProcess();
191
+ const execCwd = opts?.cwd ?? this._cwd;
192
+
193
+ const handle = this._processManager.spawn({
194
+ command: cmd,
195
+ args: args ?? [],
196
+ cwd: execCwd,
197
+ env: opts?.env ?? {},
198
+ });
199
+
200
+ handle.on("stdout", (data: string) => {
201
+ if (!proc.exited) proc._pushStdout(data);
202
+ });
203
+
204
+ handle.on("stderr", (data: string) => {
205
+ if (!proc.exited) proc._pushStderr(data);
206
+ });
207
+
208
+ handle.on("exit", (exitCode: number) => {
209
+ if (!proc.exited) proc._finish(exitCode);
210
+ });
211
+
212
+ handle.on("worker-error", (message: string) => {
213
+ if (!proc.exited) {
214
+ proc._pushStderr(`Worker error: ${message}\n`);
215
+ proc._finish(1);
216
+ }
217
+ });
218
+
219
+ proc._setSendStdin((data: string) => handle.sendStdin(data));
220
+ proc._setKillFn(() => handle.kill("SIGINT"));
221
+
222
+ if (opts?.signal) {
223
+ opts.signal.addEventListener("abort", () => {
224
+ handle.kill("SIGINT");
225
+ }, { once: true });
226
+ }
227
+
228
+ await new Promise<void>((resolve) => {
229
+ if (handle.state === "running") {
230
+ resolve();
231
+ } else {
232
+ handle.on("ready", () => resolve());
233
+ }
234
+ });
235
+
236
+ const isNodeCmd = cmd === "node" && args?.length;
237
+ if (isNodeCmd) {
238
+ const filePath = this._resolveCommand(cmd, args);
239
+ handle.exec({
240
+ type: "exec",
241
+ filePath,
242
+ args: args ?? [],
243
+ cwd: execCwd,
244
+ env: opts?.env,
245
+ isShell: false,
246
+ });
247
+ } else {
248
+ const fullCmd = args?.length ? `${cmd} ${args.join(" ")}` : cmd;
249
+ handle.exec({
250
+ type: "exec",
251
+ filePath: "",
252
+ args: args ?? [],
253
+ cwd: execCwd,
254
+ env: opts?.env,
255
+ isShell: true,
256
+ shellCommand: fullCmd,
257
+ });
258
+ }
259
+
260
+ return proc;
261
+ }
262
+
263
+ private _resolveCommand(cmd: string, args?: string[]): string {
264
+ if (cmd === "node" && args?.length) {
265
+ const filePath = args[0];
266
+ if (filePath.startsWith("/")) return filePath;
267
+ return `${this._cwd}/${filePath}`.replace(/\/+/g, "/");
268
+ }
269
+ return cmd;
270
+ }
271
+
272
+ /* ---- createTerminal() ---- */
273
+
274
+ createTerminal(opts: TerminalOptions): NodepodTerminal {
275
+ const terminal = new NodepodTerminal(opts);
276
+ terminal.setCwd(this._cwd);
277
+
278
+ let activeAbort: AbortController | null = null;
279
+ let currentSendStdin: ((data: string) => void) | null = null;
280
+ let activeCommandId = 0;
281
+ const nextCommandId = () => { activeCommandId = (activeCommandId + 1) % Number.MAX_SAFE_INTEGER; return activeCommandId; };
282
+ let isStdinRaw = false;
283
+
284
+ // Persistent shell worker -- reused across commands so VFS state persists
285
+ // and we skip the ~1s worker creation overhead per command
286
+ let shellHandle: ProcessHandle | null = null;
287
+ let shellReady: Promise<void> | null = null;
288
+
289
+ const ensureShellWorker = (): Promise<void> => {
290
+ if (shellHandle && shellHandle.state !== "exited") {
291
+ return shellReady!;
292
+ }
293
+ shellHandle = this._processManager.spawn({
294
+ command: "shell",
295
+ args: [],
296
+ cwd: this._cwd,
297
+ env: {},
298
+ });
299
+ shellReady = new Promise<void>((resolve) => {
300
+ if (shellHandle!.state === "running") {
301
+ resolve();
302
+ } else {
303
+ shellHandle!.on("ready", () => resolve());
304
+ }
305
+ });
306
+
307
+ shellHandle.on("cwd-change", (cwd: string) => {
308
+ this._cwd = cwd;
309
+ terminal.setCwd(cwd);
310
+ });
311
+
312
+ shellHandle.on("stdin-raw-status", (raw: boolean) => {
313
+ isStdinRaw = raw;
314
+ });
315
+
316
+ // Worker died -- next command will spawn a fresh one
317
+ shellHandle.on("exit", () => {
318
+ shellHandle = null;
319
+ shellReady = null;
320
+ });
321
+
322
+ return shellReady;
323
+ };
324
+
325
+ terminal._wireExecution({
326
+ onCommand: async (cmd: string) => {
327
+ const myAbort = new AbortController();
328
+ activeAbort = myAbort;
329
+ const myCommandId = nextCommandId();
330
+
331
+ let streamed = false;
332
+ let wroteNewline = false;
333
+
334
+ function ensureNewline() {
335
+ if (!wroteNewline) {
336
+ wroteNewline = true;
337
+ terminal.write("\r\n");
338
+ }
339
+ }
340
+
341
+ // Ensure persistent shell worker is running
342
+ await ensureShellWorker();
343
+ const handle = shellHandle!;
344
+
345
+ // Ignore output from previous commands or before exec is sent (stale child output)
346
+ let execSent = false;
347
+ const onStdout = (data: string) => {
348
+ if (myCommandId !== activeCommandId) return;
349
+ if (!execSent) return;
350
+ streamed = true;
351
+ ensureNewline();
352
+ terminal._writeOutput(data);
353
+ };
354
+ const onStderr = (data: string) => {
355
+ if (myCommandId !== activeCommandId) return;
356
+ if (!execSent) return;
357
+ streamed = true;
358
+ ensureNewline();
359
+ terminal._writeOutput(data, true);
360
+ };
361
+
362
+ handle.on("stdout", onStdout);
363
+ handle.on("stderr", onStderr);
364
+
365
+ currentSendStdin = (data: string) => handle.sendStdin(data);
366
+
367
+ // PM.kill() recursively kills descendants + cleans up server ports
368
+ myAbort.signal.addEventListener("abort", () => {
369
+ this._processManager.kill(handle.pid, "SIGINT");
370
+ }, { once: true });
371
+
372
+ handle.exec({
373
+ type: "exec",
374
+ filePath: "",
375
+ args: [],
376
+ cwd: this._cwd,
377
+ isShell: true,
378
+ shellCommand: cmd,
379
+ persistent: true,
380
+ });
381
+ execSent = true;
382
+
383
+ return new Promise<void>((resolve) => {
384
+ const cleanup = () => {
385
+ handle.removeListener("shell-done", onDone);
386
+ handle.removeListener("exit", onExit);
387
+ handle.removeListener("stdout", onStdout);
388
+ handle.removeListener("stderr", onStderr);
389
+ };
390
+
391
+ const onDone = (exitCode: number, stdout: string, stderr: string) => {
392
+ cleanup();
393
+ const isStale = myCommandId !== activeCommandId;
394
+ if (!isStale) {
395
+ currentSendStdin = null;
396
+ }
397
+
398
+ const aborted = myAbort.signal.aborted;
399
+
400
+ if (!aborted && !streamed && !isStale) {
401
+ const outStr = String(stdout ?? "");
402
+ const errStr = String(stderr ?? "");
403
+ if (outStr || errStr) ensureNewline();
404
+ if (outStr) terminal._writeOutput(outStr);
405
+ if (errStr) terminal._writeOutput(errStr, true);
406
+ }
407
+
408
+ if (activeAbort === myAbort) activeAbort = null;
409
+
410
+ if (!aborted && !isStale) {
411
+ if (!wroteNewline) terminal.write("\r\n");
412
+ terminal._setRunning(false);
413
+ terminal._writePrompt();
414
+ }
415
+ resolve();
416
+ };
417
+
418
+ const onExit = (exitCode: number, stdout: string, stderr: string) => {
419
+ cleanup();
420
+ const isStale = myCommandId !== activeCommandId;
421
+ if (!isStale) currentSendStdin = null;
422
+ const aborted = myAbort.signal.aborted;
423
+ if (!aborted && !streamed && !isStale) {
424
+ const outStr = String(stdout ?? "");
425
+ const errStr = String(stderr ?? "");
426
+ if (outStr || errStr) ensureNewline();
427
+ if (outStr) terminal._writeOutput(outStr);
428
+ if (errStr) terminal._writeOutput(errStr, true);
429
+ }
430
+ if (activeAbort === myAbort) activeAbort = null;
431
+ if (!aborted && !isStale) {
432
+ if (!wroteNewline) terminal.write("\r\n");
433
+ terminal._setRunning(false);
434
+ terminal._writePrompt();
435
+ }
436
+ resolve();
437
+ };
438
+
439
+ handle.on("shell-done", onDone);
440
+ handle.on("exit", onExit);
441
+ });
442
+ },
443
+
444
+ getSendStdin: () => currentSendStdin,
445
+ getIsStdinRaw: () => isStdinRaw,
446
+ getActiveAbort: () => activeAbort,
447
+ setActiveAbort: (ac) => {
448
+ activeAbort = ac;
449
+ },
450
+ });
451
+
452
+ return terminal;
453
+ }
454
+
455
+ /* ---- setPreviewScript() ---- */
456
+
457
+ // Inject a script into every preview iframe before any page content loads.
458
+ // Useful for setting up a communication bridge between the main window and
459
+ // the preview iframe, injecting polyfills, analytics, etc.
460
+ async setPreviewScript(script: string): Promise<void> {
461
+ this._proxy.setPreviewScript(script);
462
+ }
463
+
464
+ async clearPreviewScript(): Promise<void> {
465
+ this._proxy.setPreviewScript(null);
466
+ }
467
+
468
+ /* ---- port() ---- */
469
+
470
+ // Returns the preview URL for a server on this port, or null
471
+ port(num: number): string | null {
472
+ if (this._proxy.activePorts().includes(num)) {
473
+ return this._proxy.serverUrl(num);
474
+ }
475
+ return null;
476
+ }
477
+
478
+ /* ---- snapshot / restore ---- */
479
+
480
+ snapshot(): Snapshot {
481
+ return this._volume.toSnapshot();
482
+ }
483
+
484
+ restore(snapshot: Snapshot): void {
485
+ // No clearAll on MemoryVolume, so just swap the internal tree
486
+ const fresh = MemoryVolume.fromSnapshot(snapshot);
487
+ (this._volume as any).tree = (fresh as any).tree;
488
+ }
489
+
490
+ /* ---- teardown ---- */
491
+
492
+ teardown(): void {
493
+ if (this._unwatchVFS) {
494
+ this._unwatchVFS();
495
+ this._unwatchVFS = null;
496
+ }
497
+ this._engine.clearCache();
498
+ this._processManager.teardown();
499
+ }
500
+
501
+ /* ---- Escape hatches ---- */
502
+
503
+ get volume(): MemoryVolume { return this._volume; }
504
+ get engine(): ScriptEngine { return this._engine; }
505
+ get packages(): DependencyInstaller { return this._packages; }
506
+ get proxy(): RequestProxy { return this._proxy; }
507
+ get processManager(): ProcessManager { return this._processManager; }
508
+ get cwd(): string { return this._cwd; }
509
+ }
@@ -0,0 +1,70 @@
1
+ import type { VolumeSnapshot } from "../engine-types";
2
+
3
+ /* ---- Boot options ---- */
4
+
5
+ export interface NodepodOptions {
6
+ files?: Record<string, string | Uint8Array>;
7
+ env?: Record<string, string>;
8
+ workdir?: string;
9
+ swUrl?: string;
10
+ onServerReady?: (port: number, url: string) => void;
11
+ /** Show a small "nodepod" watermark link in preview iframes. Defaults to true. */
12
+ watermark?: boolean;
13
+ }
14
+
15
+ /* ---- Terminal ---- */
16
+
17
+ export interface TerminalTheme {
18
+ background?: string;
19
+ foreground?: string;
20
+ cursor?: string;
21
+ selectionBackground?: string;
22
+ black?: string;
23
+ red?: string;
24
+ green?: string;
25
+ yellow?: string;
26
+ blue?: string;
27
+ magenta?: string;
28
+ cyan?: string;
29
+ white?: string;
30
+ brightBlack?: string;
31
+ brightRed?: string;
32
+ brightGreen?: string;
33
+ brightYellow?: string;
34
+ brightBlue?: string;
35
+ brightMagenta?: string;
36
+ brightCyan?: string;
37
+ brightWhite?: string;
38
+ }
39
+
40
+ export interface TerminalOptions {
41
+ // xterm.js classes are peer deps, passed in as constructors
42
+ Terminal: any;
43
+ FitAddon?: any;
44
+ WebglAddon?: any;
45
+ theme?: TerminalTheme;
46
+ fontSize?: number;
47
+ fontFamily?: string;
48
+ prompt?: (cwd: string) => string;
49
+ }
50
+
51
+ /* ---- Filesystem ---- */
52
+
53
+ export interface StatResult {
54
+ isFile: boolean;
55
+ isDirectory: boolean;
56
+ size: number;
57
+ mtime: number;
58
+ }
59
+
60
+ /* ---- Snapshot ---- */
61
+
62
+ export type Snapshot = VolumeSnapshot;
63
+
64
+ /* ---- Spawn ---- */
65
+
66
+ export interface SpawnOptions {
67
+ cwd?: string;
68
+ env?: Record<string, string>;
69
+ signal?: AbortSignal;
70
+ }
@@ -0,0 +1,121 @@
1
+ import type { ShellCommand } from "../shell-types";
2
+ import type { PmDeps } from "./pm-types";
3
+ import { VERSIONS } from "../../constants/config";
4
+
5
+ const A_RESET = "\x1b[0m";
6
+ const A_BOLD = "\x1b[1m";
7
+ const A_DIM = "\x1b[2m";
8
+ const A_GREEN = "\x1b[32m";
9
+ const A_CYAN = "\x1b[36m";
10
+
11
+ export function createBunCommand(deps: PmDeps): ShellCommand {
12
+ return {
13
+ name: "bun",
14
+ async execute(params, ctx) {
15
+ if (!deps.hasFile("/"))
16
+ return { stdout: "", stderr: "Volume unavailable\n", exitCode: 1 };
17
+
18
+ const sub = params[0];
19
+ if (!sub || sub === "help" || sub === "--help") {
20
+ return {
21
+ stdout:
22
+ `${A_BOLD}Bun${A_RESET} is a fast JavaScript runtime, package manager, and bundler.\n\n` +
23
+ `${A_DIM}Usage:${A_RESET} bun <command> [...flags] [...args]\n\n` +
24
+ `${A_BOLD}Commands:${A_RESET}\n` +
25
+ ` ${A_CYAN}run${A_RESET} ${A_DIM}Run a package.json script or file${A_RESET}\n` +
26
+ ` ${A_CYAN}install${A_RESET} ${A_DIM}Install dependencies from package.json${A_RESET}\n` +
27
+ ` ${A_CYAN}add${A_RESET} ${A_DIM}Add a dependency${A_RESET}\n` +
28
+ ` ${A_CYAN}remove${A_RESET} ${A_DIM}Remove a dependency${A_RESET}\n` +
29
+ ` ${A_CYAN}init${A_RESET} ${A_DIM}Start an empty Bun project${A_RESET}\n` +
30
+ ` ${A_CYAN}create${A_RESET} ${A_DIM}Create a new project from a template${A_RESET}\n` +
31
+ ` ${A_CYAN}test${A_RESET} ${A_DIM}Run unit tests${A_RESET}\n` +
32
+ ` ${A_CYAN}x${A_RESET} ${A_DIM}Execute a package binary (bunx)${A_RESET}\n` +
33
+ ` ${A_CYAN}pm${A_RESET} ${A_DIM}Package manager utilities${A_RESET}\n`,
34
+ stderr: "",
35
+ exitCode: 0,
36
+ };
37
+ }
38
+
39
+ switch (sub) {
40
+ case "run":
41
+ if (
42
+ params[1] &&
43
+ (params[1].endsWith(".js") ||
44
+ params[1].endsWith(".ts") ||
45
+ params[1].endsWith(".mjs") ||
46
+ params[1].endsWith(".tsx") ||
47
+ params[1].endsWith(".jsx"))
48
+ ) {
49
+ return deps.executeNodeBinary(params[1], params.slice(2), ctx);
50
+ }
51
+ return deps.runScript(params.slice(1), ctx);
52
+ case "start":
53
+ return deps.runScript(["start"], ctx);
54
+ case "test":
55
+ case "t":
56
+ return deps.runScript(["test"], ctx);
57
+ case "install":
58
+ case "i":
59
+ return deps.installPackages(params.slice(1), ctx, "bun");
60
+ case "add":
61
+ return deps.installPackages(params.slice(1), ctx, "bun");
62
+ case "remove":
63
+ case "rm":
64
+ return deps.uninstallPackages(params.slice(1), ctx, "bun");
65
+ case "x":
66
+ return deps.npxExecute(params.slice(1), ctx);
67
+ case "init":
68
+ case "create":
69
+ return deps.npmInitOrCreate(params.slice(1), sub, ctx);
70
+ case "pm": {
71
+ const pmSub = params[1];
72
+ if (pmSub === "ls" || pmSub === "list")
73
+ return deps.listPackages(ctx, "bun");
74
+ if (pmSub === "cache")
75
+ return { stdout: "Cache path: /tmp/bun-cache\n", stderr: "", exitCode: 0 };
76
+ return {
77
+ stdout: `${A_DIM}bun pm: available subcommands: ls, cache${A_RESET}\n`,
78
+ stderr: "",
79
+ exitCode: 0,
80
+ };
81
+ }
82
+ case "version":
83
+ case "-v":
84
+ case "--version":
85
+ return { stdout: VERSIONS.BUN + "\n", stderr: "", exitCode: 0 };
86
+ case "upgrade":
87
+ return {
88
+ stdout: `${A_GREEN}Bun is already up to date.${A_RESET}\n`,
89
+ stderr: "",
90
+ exitCode: 0,
91
+ };
92
+ default: {
93
+ // bare `bun <file>` runs it directly, otherwise treat as script
94
+ if (params[0] && !params[0].startsWith("-")) {
95
+ const filePath = params[0].startsWith("/")
96
+ ? params[0]
97
+ : `${ctx.cwd}/${params[0]}`.replace(/\/+/g, "/");
98
+ if (deps.hasFile(filePath)) {
99
+ return deps.executeNodeBinary(params[0], params.slice(1), ctx);
100
+ }
101
+ return deps.runScript(params, ctx);
102
+ }
103
+ return {
104
+ stdout: "",
105
+ stderr: `error: unknown command "${sub}"\n`,
106
+ exitCode: 1,
107
+ };
108
+ }
109
+ }
110
+ },
111
+ };
112
+ }
113
+
114
+ export function createBunxCommand(deps: PmDeps): ShellCommand {
115
+ return {
116
+ name: "bunx",
117
+ async execute(params, ctx) {
118
+ return deps.npxExecute(params, ctx);
119
+ },
120
+ };
121
+ }