@scelar/nodepod 1.0.6 → 1.0.7

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.
@@ -1,7 +1,10 @@
1
1
  import { MemoryVolume } from "../memory-volume";
2
- import { ScriptEngine } from "../script-engine";
3
2
  import { DependencyInstaller } from "../packages/installer";
4
- import { RequestProxy, getProxyInstance, type IVirtualServer } from "../request-proxy";
3
+ import {
4
+ RequestProxy,
5
+ getProxyInstance,
6
+ type IVirtualServer,
7
+ } from "../request-proxy";
5
8
  import type { VolumeSnapshot } from "../engine-types";
6
9
  import { Buffer } from "../polyfills/buffer";
7
10
  import type {
@@ -17,23 +20,18 @@ import { NodepodTerminal } from "./nodepod-terminal";
17
20
  import { ProcessManager } from "../threading/process-manager";
18
21
  import type { ProcessHandle } from "../threading/process-handle";
19
22
  import { VFSBridge } from "../threading/vfs-bridge";
20
- import { isSharedArrayBufferAvailable, SharedVFSController } from "../threading/shared-vfs";
23
+ import {
24
+ isSharedArrayBufferAvailable,
25
+ SharedVFSController,
26
+ } from "../threading/shared-vfs";
21
27
  import { SyncChannelController } from "../threading/sync-channel";
22
28
  import { MemoryHandler } from "../memory-handler";
23
29
  import { openSnapshotCache } from "../persistence/idb-cache";
24
30
 
25
- // Lazy-load child_process so the shell doesn't get pulled in at import time
26
- let _shellMod: typeof import("../polyfills/child_process") | null = null;
27
- async function getShellMod() {
28
- if (!_shellMod) _shellMod = await import("../polyfills/child_process");
29
- return _shellMod;
30
- }
31
-
32
31
  export class Nodepod {
33
32
  readonly fs: NodepodFS;
34
33
 
35
34
  private _volume: MemoryVolume;
36
- private _engine: ScriptEngine;
37
35
  private _packages: DependencyInstaller;
38
36
  private _proxy: RequestProxy;
39
37
  private _cwd: string;
@@ -49,14 +47,12 @@ export class Nodepod {
49
47
 
50
48
  private constructor(
51
49
  volume: MemoryVolume,
52
- engine: ScriptEngine,
53
50
  packages: DependencyInstaller,
54
51
  proxy: RequestProxy,
55
52
  cwd: string,
56
53
  handler: MemoryHandler,
57
54
  ) {
58
55
  this._volume = volume;
59
- this._engine = engine;
60
56
  this._packages = packages;
61
57
  this._proxy = proxy;
62
58
  this._cwd = cwd;
@@ -67,7 +63,12 @@ export class Nodepod {
67
63
 
68
64
  this._vfsBridge.setBroadcaster((path, content, excludePid) => {
69
65
  const isDirectory = content !== null && content.byteLength === 0;
70
- this._processManager.broadcastVFSChange(path, content, isDirectory, excludePid);
66
+ this._processManager.broadcastVFSChange(
67
+ path,
68
+ content,
69
+ isDirectory,
70
+ excludePid,
71
+ );
71
72
  });
72
73
 
73
74
  this._processManager.setVFSBridge(this._vfsBridge);
@@ -93,29 +94,41 @@ export class Nodepod {
93
94
  }
94
95
 
95
96
  // Bridge worker HTTP servers to the RequestProxy for preview URLs
96
- this._processManager.on("server-listen", (_pid: number, port: number, _hostname: string) => {
97
- const proxyServer: IVirtualServer = {
98
- listening: true,
99
- address: () => ({ port, address: "0.0.0.0", family: "IPv4" }),
100
- dispatchRequest: async (method, url, headers, body) => {
101
- const bodyStr = body ? (typeof body === "string" ? body : body.toString("utf8")) : null;
102
- const result = await this._processManager.dispatchHttpRequest(
103
- port, method, url, headers, bodyStr,
104
- );
105
- // Body can be ArrayBuffer (binary) or string (text)
106
- const respBody = result.body instanceof ArrayBuffer
107
- ? Buffer.from(new Uint8Array(result.body))
108
- : Buffer.from(result.body);
109
- return {
110
- statusCode: result.statusCode,
111
- statusMessage: result.statusMessage,
112
- headers: result.headers,
113
- body: respBody,
114
- };
115
- },
116
- };
117
- this._proxy.register(proxyServer, port);
118
- });
97
+ this._processManager.on(
98
+ "server-listen",
99
+ (_pid: number, port: number, _hostname: string) => {
100
+ const proxyServer: IVirtualServer = {
101
+ listening: true,
102
+ address: () => ({ port, address: "0.0.0.0", family: "IPv4" }),
103
+ dispatchRequest: async (method, url, headers, body) => {
104
+ const bodyStr = body
105
+ ? typeof body === "string"
106
+ ? body
107
+ : body.toString("utf8")
108
+ : null;
109
+ const result = await this._processManager.dispatchHttpRequest(
110
+ port,
111
+ method,
112
+ url,
113
+ headers,
114
+ bodyStr,
115
+ );
116
+ // Body can be ArrayBuffer (binary) or string (text)
117
+ const respBody =
118
+ result.body instanceof ArrayBuffer
119
+ ? Buffer.from(new Uint8Array(result.body))
120
+ : Buffer.from(result.body);
121
+ return {
122
+ statusCode: result.statusCode,
123
+ statusMessage: result.statusMessage,
124
+ headers: result.headers,
125
+ body: respBody,
126
+ };
127
+ },
128
+ };
129
+ this._proxy.register(proxyServer, port);
130
+ },
131
+ );
119
132
 
120
133
  this._processManager.on("server-close", (_pid: number, port: number) => {
121
134
  this._proxy.unregister(port);
@@ -128,26 +141,29 @@ export class Nodepod {
128
141
 
129
142
  static async boot(opts: NodepodOptions = {}): Promise<Nodepod> {
130
143
  if (typeof Worker === "undefined") {
131
- throw new Error("[Nodepod] Web Workers are required. Nodepod cannot run without Web Worker support.");
144
+ throw new Error(
145
+ "[Nodepod] Web Workers are required. Nodepod cannot run without Web Worker support.",
146
+ );
132
147
  }
133
148
  if (typeof SharedArrayBuffer === "undefined") {
134
- 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).");
149
+ throw new Error(
150
+ "[Nodepod] SharedArrayBuffer is required. Ensure Cross-Origin-Isolation headers are set (Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: credentialless).",
151
+ );
135
152
  }
136
153
 
137
154
  const cwd = opts.workdir ?? "/";
138
155
  const handler = new MemoryHandler(opts.memory);
139
156
  handler.startMonitoring();
140
157
  const volume = new MemoryVolume(handler);
141
- const engine = new ScriptEngine(volume, {
142
- cwd,
143
- env: opts.env,
144
- handler,
145
- });
146
158
 
147
159
  // Open IDB snapshot cache for faster re-boots (opt-out via enableSnapshotCache: false)
148
160
  let snapshotCache = null;
149
161
  if (opts.enableSnapshotCache !== false) {
150
- try { snapshotCache = await openSnapshotCache(); } catch { /* IDB unavailable */ }
162
+ try {
163
+ snapshotCache = await openSnapshotCache();
164
+ } catch {
165
+ /* IDB unavailable */
166
+ }
151
167
  }
152
168
 
153
169
  const packages = new DependencyInstaller(volume, { snapshotCache });
@@ -155,10 +171,7 @@ export class Nodepod {
155
171
  onServerReady: opts.onServerReady,
156
172
  });
157
173
 
158
- const nodepod = new Nodepod(volume, engine, packages, proxy, cwd, handler);
159
-
160
- // Drop module cache under memory pressure (safe — modules re-execute on next require)
161
- handler.onPressure(() => engine.clearCache());
174
+ const nodepod = new Nodepod(volume, packages, proxy, cwd, handler);
162
175
 
163
176
  if (opts.files) {
164
177
  for (const [path, content] of Object.entries(opts.files)) {
@@ -180,9 +193,6 @@ export class Nodepod {
180
193
  }
181
194
  }
182
195
 
183
- const shell = await getShellMod();
184
- shell.initShellExec(volume, { cwd, env: opts.env });
185
-
186
196
  if (
187
197
  opts.swUrl &&
188
198
  typeof navigator !== "undefined" &&
@@ -243,9 +253,13 @@ export class Nodepod {
243
253
  proc._setKillFn(() => handle.kill("SIGINT"));
244
254
 
245
255
  if (opts?.signal) {
246
- opts.signal.addEventListener("abort", () => {
247
- handle.kill("SIGINT");
248
- }, { once: true });
256
+ opts.signal.addEventListener(
257
+ "abort",
258
+ () => {
259
+ handle.kill("SIGINT");
260
+ },
261
+ { once: true },
262
+ );
249
263
  }
250
264
 
251
265
  await new Promise<void>((resolve) => {
@@ -301,7 +315,10 @@ export class Nodepod {
301
315
  let activeAbort: AbortController | null = null;
302
316
  let currentSendStdin: ((data: string) => void) | null = null;
303
317
  let activeCommandId = 0;
304
- const nextCommandId = () => { activeCommandId = (activeCommandId + 1) % Number.MAX_SAFE_INTEGER; return activeCommandId; };
318
+ const nextCommandId = () => {
319
+ activeCommandId = (activeCommandId + 1) % Number.MAX_SAFE_INTEGER;
320
+ return activeCommandId;
321
+ };
305
322
  let isStdinRaw = false;
306
323
 
307
324
  // Persistent shell worker -- reused across commands so VFS state persists
@@ -388,9 +405,13 @@ export class Nodepod {
388
405
  currentSendStdin = (data: string) => handle.sendStdin(data);
389
406
 
390
407
  // PM.kill() recursively kills descendants + cleans up server ports
391
- myAbort.signal.addEventListener("abort", () => {
392
- this._processManager.kill(handle.pid, "SIGINT");
393
- }, { once: true });
408
+ myAbort.signal.addEventListener(
409
+ "abort",
410
+ () => {
411
+ this._processManager.kill(handle.pid, "SIGINT");
412
+ },
413
+ { once: true },
414
+ );
394
415
 
395
416
  handle.exec({
396
417
  type: "exec",
@@ -501,11 +522,18 @@ export class Nodepod {
501
522
  /* ---- snapshot / restore ---- */
502
523
 
503
524
  /** Directory names excluded from snapshots at any depth when shallow=true. */
504
- private static readonly SHALLOW_EXCLUDE_DIRS = new Set(['node_modules', '.cache', '.npm']);
525
+ private static readonly SHALLOW_EXCLUDE_DIRS = new Set([
526
+ "node_modules",
527
+ ".cache",
528
+ ".npm",
529
+ ]);
505
530
 
506
531
  snapshot(opts?: SnapshotOptions): Snapshot {
507
532
  const shallow = opts?.shallow ?? true;
508
- return this._volume.toSnapshot(undefined, shallow ? Nodepod.SHALLOW_EXCLUDE_DIRS : undefined);
533
+ return this._volume.toSnapshot(
534
+ undefined,
535
+ shallow ? Nodepod.SHALLOW_EXCLUDE_DIRS : undefined,
536
+ );
509
537
  }
510
538
 
511
539
  async restore(snapshot: Snapshot, opts?: SnapshotOptions): Promise<void> {
@@ -516,7 +544,7 @@ export class Nodepod {
516
544
  (this._volume as any).tree = (fresh as any).tree;
517
545
 
518
546
  // Auto-install deps from package.json if requested and manifest exists
519
- if (autoInstall && this._volume.existsSync('/package.json')) {
547
+ if (autoInstall && this._volume.existsSync("/package.json")) {
520
548
  await this._packages.installFromManifest();
521
549
  }
522
550
  }
@@ -528,7 +556,6 @@ export class Nodepod {
528
556
  this._unwatchVFS();
529
557
  this._unwatchVFS = null;
530
558
  }
531
- this._engine.clearCache();
532
559
  this._processManager.teardown();
533
560
  this._volume.dispose();
534
561
  this._handler.destroy();
@@ -537,24 +564,27 @@ export class Nodepod {
537
564
  /* ---- Performance stats ---- */
538
565
 
539
566
  memoryStats(): {
540
- vfs: { fileCount: number; totalBytes: number; dirCount: number; watcherCount: number };
567
+ vfs: {
568
+ fileCount: number;
569
+ totalBytes: number;
570
+ dirCount: number;
571
+ watcherCount: number;
572
+ };
541
573
  engine: { moduleCacheSize: number; transformCacheSize: number };
542
574
  heap: { usedMB: number; totalMB: number; limitMB: number } | null;
543
575
  } {
544
576
  const vfs = this._volume.getStats();
545
- const moduleRegistry = (this._engine as any).moduleRegistry ?? {};
546
- const transformCache: Map<string, string> = (this._engine as any).transformCache;
547
- const engine = {
548
- moduleCacheSize: Object.keys(moduleRegistry).length,
549
- transformCacheSize: transformCache?.size ?? 0,
550
- };
551
- let heap: { usedMB: number; totalMB: number; limitMB: number } | null = null;
552
- const perf = typeof performance !== 'undefined' ? (performance as any) : null;
577
+ // Engine stats are per-worker; main thread no longer runs a ScriptEngine
578
+ const engine = { moduleCacheSize: 0, transformCacheSize: 0 };
579
+ let heap: { usedMB: number; totalMB: number; limitMB: number } | null =
580
+ null;
581
+ const perf =
582
+ typeof performance !== "undefined" ? (performance as any) : null;
553
583
  if (perf?.memory) {
554
584
  heap = {
555
- usedMB: Math.round(perf.memory.usedJSHeapSize / 1048576 * 10) / 10,
556
- totalMB: Math.round(perf.memory.totalJSHeapSize / 1048576 * 10) / 10,
557
- limitMB: Math.round(perf.memory.jsHeapSizeLimit / 1048576 * 10) / 10,
585
+ usedMB: Math.round((perf.memory.usedJSHeapSize / 1048576) * 10) / 10,
586
+ totalMB: Math.round((perf.memory.totalJSHeapSize / 1048576) * 10) / 10,
587
+ limitMB: Math.round((perf.memory.jsHeapSizeLimit / 1048576) * 10) / 10,
558
588
  };
559
589
  }
560
590
  return { vfs, engine, heap };
@@ -562,10 +592,26 @@ export class Nodepod {
562
592
 
563
593
  /* ---- Escape hatches ---- */
564
594
 
565
- get volume(): MemoryVolume { return this._volume; }
566
- get engine(): ScriptEngine { return this._engine; }
567
- get packages(): DependencyInstaller { return this._packages; }
568
- get proxy(): RequestProxy { return this._proxy; }
569
- get processManager(): ProcessManager { return this._processManager; }
570
- get cwd(): string { return this._cwd; }
595
+ get volume(): MemoryVolume {
596
+ return this._volume;
597
+ }
598
+ /** @deprecated Main-thread engine removed for security. all code now runs in isolated Web Workers via spawn() <-- this removes fatal security flaws. */
599
+ get engine(): never {
600
+ throw new Error(
601
+ "[Nodepod] Main-thread engine removed for security. " +
602
+ "All code now runs in isolated Web Workers via spawn().",
603
+ );
604
+ }
605
+ get packages(): DependencyInstaller {
606
+ return this._packages;
607
+ }
608
+ get proxy(): RequestProxy {
609
+ return this._proxy;
610
+ }
611
+ get processManager(): ProcessManager {
612
+ return this._processManager;
613
+ }
614
+ get cwd(): string {
615
+ return this._cwd;
616
+ }
571
617
  }