@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.
- package/dist/{child_process-4ZrgCVFu.js → child_process-CgnmoilU.js} +4 -4
- package/dist/{child_process-4ZrgCVFu.js.map → child_process-CgnmoilU.js.map} +1 -1
- package/dist/{child_process-Cao4lyrb.cjs → child_process-bGGe8mTj.cjs} +5 -5
- package/dist/{child_process-Cao4lyrb.cjs.map → child_process-bGGe8mTj.cjs.map} +1 -1
- package/dist/{index-HkVqijtm.js → index-DZpqX03n.js} +11 -35
- package/dist/{index-HkVqijtm.js.map → index-DZpqX03n.js.map} +1 -1
- package/dist/{index-DuYo2yDs.cjs → index-NinyWmnj.cjs} +12 -30
- package/dist/{index-DuYo2yDs.cjs.map → index-NinyWmnj.cjs.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +2 -2
- package/dist/polyfills/readline.d.ts +108 -108
- package/dist/sdk/nodepod.d.ts +58 -59
- package/package.json +97 -97
- package/src/sdk/nodepod.ts +127 -81
package/src/sdk/nodepod.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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(
|
|
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(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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(
|
|
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(
|
|
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 {
|
|
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,
|
|
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(
|
|
247
|
-
|
|
248
|
-
|
|
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 = () => {
|
|
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(
|
|
392
|
-
|
|
393
|
-
|
|
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([
|
|
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(
|
|
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(
|
|
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: {
|
|
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
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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 {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
get
|
|
570
|
-
|
|
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
|
}
|