@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,56 @@
1
+ // shared types for the script engine and its execution modes
2
+
3
+ import type { MemoryVolume } from './memory-volume';
4
+
5
+ export interface EngineConfig {
6
+ cwd?: string;
7
+ env?: Record<string, string>;
8
+ onConsole?: (method: string, args: unknown[]) => void;
9
+ onStdout?: (data: string) => void;
10
+ onStderr?: (data: string) => void;
11
+ }
12
+
13
+ export interface LoadedModule {
14
+ id: string;
15
+ filename: string;
16
+ exports: unknown;
17
+ loaded: boolean;
18
+ children: LoadedModule[];
19
+ paths: string[];
20
+ }
21
+
22
+ export interface ExecutionOutcome {
23
+ exports: unknown;
24
+ module: LoadedModule;
25
+ }
26
+
27
+ // async interface implemented by all execution modes (main thread, worker, iframe)
28
+ export interface IScriptEngine {
29
+ execute(code: string, filename?: string): Promise<ExecutionOutcome>;
30
+ runFile(filename: string): Promise<ExecutionOutcome>;
31
+ clearCache(): void;
32
+ getVolume?(): MemoryVolume;
33
+ terminate?(): void;
34
+ }
35
+
36
+ export interface SpawnEngineConfig extends EngineConfig {
37
+ // cross-origin sandbox URL for maximum isolation (prevents cookie/storage access)
38
+ sandboxUrl?: string;
39
+
40
+ // explicitly permit same-origin eval (required when sandboxUrl is not set)
41
+ allowUnsafeEval?: boolean;
42
+
43
+ // false (default) = main thread, true = Worker, 'auto' = Worker if available
44
+ useWorker?: boolean | 'auto';
45
+ }
46
+
47
+ // serialized MemoryVolume for transfer across boundaries
48
+ export interface VolumeSnapshot {
49
+ entries: VolumeEntry[];
50
+ }
51
+
52
+ export interface VolumeEntry {
53
+ path: string;
54
+ kind: 'file' | 'directory';
55
+ data?: string; // base64-encoded
56
+ }
@@ -0,0 +1,39 @@
1
+ // Chunked to avoid blowing the call stack on large buffers
2
+ const SEGMENT_SIZE = 8192;
3
+
4
+ export function bytesToBase64(data: Uint8Array): string {
5
+ const segments: string[] = [];
6
+ for (let offset = 0; offset < data.length; offset += SEGMENT_SIZE) {
7
+ segments.push(
8
+ String.fromCharCode.apply(null, Array.from(data.subarray(offset, offset + SEGMENT_SIZE)))
9
+ );
10
+ }
11
+ return btoa(segments.join(''));
12
+ }
13
+
14
+ export function base64ToBytes(encoded: string): Uint8Array {
15
+ const raw = atob(encoded);
16
+ const result = new Uint8Array(raw.length);
17
+ for (let i = 0; i < raw.length; i++) {
18
+ result[i] = raw.charCodeAt(i);
19
+ }
20
+ return result;
21
+ }
22
+
23
+ export function bytesToHex(data: Uint8Array): string {
24
+ const chars = new Array(data.length);
25
+ for (let i = 0; i < data.length; i++) {
26
+ chars[i] = data[i].toString(16).padStart(2, '0');
27
+ }
28
+ return chars.join('');
29
+ }
30
+
31
+ export function bytesToLatin1(data: Uint8Array): string {
32
+ const segments: string[] = [];
33
+ for (let offset = 0; offset < data.length; offset += SEGMENT_SIZE) {
34
+ segments.push(
35
+ String.fromCharCode.apply(null, Array.from(data.subarray(offset, offset + SEGMENT_SIZE)))
36
+ );
37
+ }
38
+ return segments.join('');
39
+ }
@@ -0,0 +1,9 @@
1
+ // DJB2 hash for cache keys
2
+ export function quickDigest(input: string): string {
3
+ let accumulator = 0;
4
+ for (let i = 0; i < input.length; i++) {
5
+ accumulator = ((accumulator << 5) - accumulator) + input.charCodeAt(i);
6
+ accumulator |= 0;
7
+ }
8
+ return accumulator.toString(36);
9
+ }
@@ -0,0 +1,96 @@
1
+ // Event loop ref-counting, mirroring Node.js/libuv semantics.
2
+ // Process stays alive while refCount > 0 (servers, readline, child procs).
3
+ // Supports both global mode and per-ProcessContext mode.
4
+
5
+ import type { ProcessContext } from "../threading/process-context";
6
+ import { getActiveContext } from "../threading/process-context";
7
+
8
+ /* ---- Global state (fallback when no ProcessContext) ---- */
9
+
10
+ let _refCount = 0;
11
+ const _drainListeners = new Set<() => void>();
12
+
13
+ function effectiveDrainListeners(): Set<() => void> {
14
+ const ctx = getActiveContext();
15
+ return ctx?.drainListeners ?? _drainListeners;
16
+ }
17
+
18
+ /* ---- Public API ---- */
19
+
20
+ export function addDrainListener(cb: () => void): () => void {
21
+ const listeners = effectiveDrainListeners();
22
+ listeners.add(cb);
23
+ return () => listeners.delete(cb);
24
+ }
25
+
26
+ export function notifyDrain(): void {
27
+ const ctx = getActiveContext();
28
+ if (ctx) {
29
+ for (const cb of ctx.drainListeners) cb();
30
+ }
31
+ // Always notify global too (ProcessManager observers)
32
+ for (const cb of _drainListeners) cb();
33
+ }
34
+
35
+ export function ref(): void {
36
+ const ctx = getActiveContext();
37
+ if (ctx) {
38
+ ctx.refCount++;
39
+ } else {
40
+ _refCount++;
41
+ }
42
+ }
43
+
44
+ export function unref(): void {
45
+ const ctx = getActiveContext();
46
+ if (ctx) {
47
+ if (ctx.refCount > 0) ctx.refCount--;
48
+ } else {
49
+ if (_refCount > 0) _refCount--;
50
+ }
51
+ notifyDrain();
52
+ }
53
+
54
+ export function getRefCount(): number {
55
+ const ctx = getActiveContext();
56
+ return ctx ? ctx.refCount : _refCount;
57
+ }
58
+
59
+ export function setRefCount(n: number): void {
60
+ const ctx = getActiveContext();
61
+ if (ctx) {
62
+ ctx.refCount = n;
63
+ } else {
64
+ _refCount = n;
65
+ }
66
+ }
67
+
68
+ export function resetRefCount(): void {
69
+ const ctx = getActiveContext();
70
+ if (ctx) {
71
+ ctx.refCount = 0;
72
+ } else {
73
+ _refCount = 0;
74
+ }
75
+ }
76
+
77
+ /* ---- Context-specific API ---- */
78
+
79
+ export function refCtx(ctx: ProcessContext): void {
80
+ ctx.refCount++;
81
+ }
82
+
83
+ export function unrefCtx(ctx: ProcessContext): void {
84
+ if (ctx.refCount > 0) ctx.refCount--;
85
+ for (const cb of ctx.drainListeners) cb();
86
+ for (const cb of _drainListeners) cb();
87
+ }
88
+
89
+ export function getRefCountCtx(ctx: ProcessContext): number {
90
+ return ctx.refCount;
91
+ }
92
+
93
+ export function addDrainListenerCtx(ctx: ProcessContext, cb: () => void): () => void {
94
+ ctx.drainListeners.add(cb);
95
+ return () => ctx.drainListeners.delete(cb);
96
+ }
@@ -0,0 +1,133 @@
1
+ // WASM compilation cache. Browsers block sync WebAssembly.Module() for large
2
+ // buffers on the main thread, so we either precompile in the background or
3
+ // offload to a worker where there's no size limit.
4
+
5
+ const PRECOMPILE_THRESHOLD = 4 * 1024 * 1024; // 4 MB
6
+
7
+ type CacheEntry = {
8
+ promise: Promise<WebAssembly.Module>;
9
+ module: WebAssembly.Module | null;
10
+ };
11
+
12
+ // Keyed by byte length -- good enough since multiple same-size .wasm files are rare
13
+ const sizeCache = new Map<number, CacheEntry>();
14
+
15
+ function actualByteLength(bytes: BufferSource): number {
16
+ if (bytes instanceof ArrayBuffer) return bytes.byteLength;
17
+ return (bytes as ArrayBufferView).byteLength;
18
+ }
19
+
20
+ function toUint8(bytes: Uint8Array | ArrayBuffer): Uint8Array {
21
+ if (bytes instanceof ArrayBuffer) return new Uint8Array(bytes);
22
+ if (
23
+ bytes.byteOffset !== 0 ||
24
+ bytes.byteLength !== (bytes.buffer as ArrayBuffer).byteLength
25
+ ) {
26
+ return new Uint8Array(
27
+ bytes.buffer as ArrayBuffer,
28
+ bytes.byteOffset,
29
+ bytes.byteLength,
30
+ );
31
+ }
32
+ return bytes;
33
+ }
34
+
35
+ function toArrayBuffer(bytes: Uint8Array | ArrayBuffer): ArrayBuffer {
36
+ if (bytes instanceof ArrayBuffer) return bytes;
37
+ return bytes.buffer.slice(
38
+ bytes.byteOffset,
39
+ bytes.byteOffset + bytes.byteLength,
40
+ ) as ArrayBuffer;
41
+ }
42
+
43
+ // Call as early as possible (e.g. when writing .wasm to VFS)
44
+ export function precompileWasm(bytes: Uint8Array | ArrayBuffer): void {
45
+ if (typeof WebAssembly === "undefined") return;
46
+
47
+ const len = bytes instanceof ArrayBuffer ? bytes.byteLength : bytes.byteLength;
48
+ if (len < PRECOMPILE_THRESHOLD) return;
49
+ if (sizeCache.has(len)) return;
50
+
51
+ const view = toUint8(bytes);
52
+ const entry: CacheEntry = {
53
+ promise: WebAssembly.compile(view as BufferSource),
54
+ module: null,
55
+ };
56
+ entry.promise.then(
57
+ (m) => { entry.module = m; },
58
+ () => {},
59
+ );
60
+ sizeCache.set(len, entry);
61
+ }
62
+
63
+ export function getCachedModule(bytes: BufferSource): WebAssembly.Module | null {
64
+ const len = actualByteLength(bytes);
65
+ const entry = sizeCache.get(len);
66
+ if (entry?.module) return entry.module;
67
+ return null;
68
+ }
69
+
70
+ // Worker-based compilation (no size limit in workers)
71
+ let _workerUrl: string | null = null;
72
+
73
+ function getWorkerUrl(): string {
74
+ if (_workerUrl) return _workerUrl;
75
+ const code = `
76
+ self.onmessage = function(e) {
77
+ try {
78
+ var mod = new WebAssembly.Module(e.data.bytes);
79
+ self.postMessage({ ok: true, module: mod });
80
+ } catch (err) {
81
+ self.postMessage({ ok: false, error: err.message });
82
+ }
83
+ };
84
+ `;
85
+ _workerUrl = URL.createObjectURL(
86
+ new Blob([code], { type: "application/javascript" }),
87
+ );
88
+ return _workerUrl;
89
+ }
90
+
91
+ export function compileWasmInWorker(
92
+ bytes: Uint8Array | ArrayBuffer,
93
+ ): Promise<WebAssembly.Module> {
94
+ return new Promise((resolve, reject) => {
95
+ try {
96
+ const worker = new Worker(getWorkerUrl());
97
+ worker.onmessage = (e: MessageEvent) => {
98
+ worker.terminate();
99
+ if (e.data.ok) {
100
+ const len = bytes instanceof ArrayBuffer ? bytes.byteLength : bytes.byteLength;
101
+ const entry = sizeCache.get(len);
102
+ if (entry) {
103
+ entry.module = e.data.module;
104
+ } else {
105
+ sizeCache.set(len, {
106
+ promise: Promise.resolve(e.data.module),
107
+ module: e.data.module,
108
+ });
109
+ }
110
+ resolve(e.data.module);
111
+ } else {
112
+ reject(new Error(e.data.error));
113
+ }
114
+ };
115
+ worker.onerror = (e) => {
116
+ worker.terminate();
117
+ reject(new Error(e.message || "Worker compilation failed"));
118
+ };
119
+ const ab = toArrayBuffer(bytes);
120
+ worker.postMessage({ bytes: ab }, [ab]);
121
+ } catch (e) {
122
+ // No workers -- fall back to async compile
123
+ const view = toUint8(bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : bytes);
124
+ WebAssembly.compile(view as BufferSource).then(resolve, reject);
125
+ }
126
+ });
127
+ }
128
+
129
+ export function needsAsyncCompile(bytes: BufferSource): boolean {
130
+ return actualByteLength(bytes) >= PRECOMPILE_THRESHOLD;
131
+ }
132
+
133
+ export { PRECOMPILE_THRESHOLD };
@@ -0,0 +1,141 @@
1
+ // executes code in a cross-origin iframe for maximum browser isolation
2
+
3
+ import type { MemoryVolume } from './memory-volume';
4
+ import type { IScriptEngine, ExecutionOutcome, EngineConfig, VolumeSnapshot } from './engine-types';
5
+
6
+ interface CrossOriginMessage {
7
+ type: 'init' | 'execute' | 'runFile' | 'clearCache' | 'syncFile' | 'ready' | 'result' | 'error' | 'console';
8
+ id?: string;
9
+ code?: string;
10
+ filename?: string;
11
+ snapshot?: VolumeSnapshot;
12
+ config?: EngineConfig;
13
+ result?: ExecutionOutcome;
14
+ error?: string;
15
+ path?: string;
16
+ content?: string | null;
17
+ consoleMethod?: string;
18
+ consoleArgs?: unknown[];
19
+ }
20
+
21
+ export class IframeSandbox implements IScriptEngine {
22
+ private frame: HTMLIFrameElement;
23
+ private targetOrigin: string;
24
+ private vol: MemoryVolume;
25
+ private cfg: EngineConfig;
26
+ private ready: Promise<void>;
27
+ private pendingCalls = new Map<string, { resolve: (r: ExecutionOutcome) => void; reject: (e: Error) => void }>();
28
+ private nextId = 0;
29
+ private onFileChange: ((p: string, c: string) => void) | null = null;
30
+ private onFileDelete: ((p: string) => void) | null = null;
31
+ private onMessage: ((e: MessageEvent) => void) | null = null;
32
+
33
+ constructor(sandboxUrl: string, vol: MemoryVolume, cfg: EngineConfig = {}) {
34
+ this.targetOrigin = new URL(sandboxUrl).origin;
35
+ this.vol = vol;
36
+ this.cfg = cfg;
37
+
38
+ this.frame = document.createElement('iframe');
39
+ this.frame.src = sandboxUrl;
40
+ this.frame.style.display = 'none';
41
+ // @ts-expect-error - credentialless attribute may not exist in types
42
+ this.frame.credentialless = true;
43
+ this.frame.setAttribute('credentialless', '');
44
+ document.body.appendChild(this.frame);
45
+
46
+ this.bindMessageHandler();
47
+ this.ready = this.awaitReady().then(() => this.sendInit());
48
+ this.attachVolumeSync();
49
+ }
50
+
51
+ private bindMessageHandler(): void {
52
+ this.onMessage = (event: MessageEvent) => {
53
+ if (event.origin !== this.targetOrigin) return;
54
+ const msg = event.data as CrossOriginMessage;
55
+
56
+ if (msg.type === 'result' && msg.id) {
57
+ const pending = this.pendingCalls.get(msg.id);
58
+ if (pending && msg.result) { pending.resolve(msg.result); this.pendingCalls.delete(msg.id); }
59
+ } else if (msg.type === 'error' && msg.id) {
60
+ const pending = this.pendingCalls.get(msg.id);
61
+ if (pending) { pending.reject(new Error(msg.error || 'Sandbox error')); this.pendingCalls.delete(msg.id); }
62
+ } else if (msg.type === 'console' && this.cfg.onConsole) {
63
+ this.cfg.onConsole(msg.consoleMethod || 'log', msg.consoleArgs || []);
64
+ }
65
+ };
66
+ window.addEventListener('message', this.onMessage);
67
+ }
68
+
69
+ private awaitReady(): Promise<void> {
70
+ return new Promise(resolve => {
71
+ const handler = (event: MessageEvent) => {
72
+ if (event.origin !== this.targetOrigin) return;
73
+ if ((event.data as CrossOriginMessage).type === 'ready') {
74
+ window.removeEventListener('message', handler);
75
+ resolve();
76
+ }
77
+ };
78
+ window.addEventListener('message', handler);
79
+ });
80
+ }
81
+
82
+ private async sendInit(): Promise<void> {
83
+ const msg: CrossOriginMessage = {
84
+ type: 'init',
85
+ snapshot: this.vol.toSnapshot(),
86
+ config: { cwd: this.cfg.cwd, env: this.cfg.env },
87
+ };
88
+ this.frame.contentWindow?.postMessage(msg, this.targetOrigin);
89
+ }
90
+
91
+ private attachVolumeSync(): void {
92
+ this.onFileChange = (path, content) => {
93
+ this.frame.contentWindow?.postMessage({ type: 'syncFile', path, content } as CrossOriginMessage, this.targetOrigin);
94
+ };
95
+ this.vol.on('change', this.onFileChange);
96
+
97
+ this.onFileDelete = (path) => {
98
+ this.frame.contentWindow?.postMessage({ type: 'syncFile', path, content: null } as CrossOriginMessage, this.targetOrigin);
99
+ };
100
+ this.vol.on('delete', this.onFileDelete);
101
+ }
102
+
103
+ private dispatch(msg: CrossOriginMessage): Promise<ExecutionOutcome> {
104
+ return new Promise((resolve, reject) => {
105
+ const id = String(this.nextId++);
106
+ this.pendingCalls.set(id, { resolve, reject });
107
+ this.frame.contentWindow?.postMessage({ ...msg, id }, this.targetOrigin);
108
+ setTimeout(() => {
109
+ if (this.pendingCalls.has(id)) {
110
+ this.pendingCalls.delete(id);
111
+ reject(new Error('Sandbox execution timeout'));
112
+ }
113
+ }, 60000);
114
+ });
115
+ }
116
+
117
+ async execute(code: string, filename?: string): Promise<ExecutionOutcome> {
118
+ await this.ready;
119
+ return this.dispatch({ type: 'execute', code, filename });
120
+ }
121
+
122
+ async runFile(filename: string): Promise<ExecutionOutcome> {
123
+ await this.ready;
124
+ return this.dispatch({ type: 'runFile', filename });
125
+ }
126
+
127
+ clearCache(): void {
128
+ this.frame.contentWindow?.postMessage({ type: 'clearCache' } as CrossOriginMessage, this.targetOrigin);
129
+ }
130
+
131
+ getVolume(): MemoryVolume { return this.vol; }
132
+
133
+ terminate(): void {
134
+ if (this.onFileChange) this.vol.off('change', this.onFileChange);
135
+ if (this.onFileDelete) this.vol.off('delete', this.onFileDelete);
136
+ if (this.onMessage) window.removeEventListener('message', this.onMessage);
137
+ this.frame.remove();
138
+ for (const [, { reject }] of this.pendingCalls) reject(new Error('Sandbox terminated'));
139
+ this.pendingCalls.clear();
140
+ }
141
+ }
package/src/index.ts ADDED
@@ -0,0 +1,192 @@
1
+ // nodepod - browser-native Node.js runtime environment
2
+
3
+ export { MemoryVolume } from "./memory-volume";
4
+ export type {
5
+ VolumeNode,
6
+ FileStat,
7
+ FileWatchHandle,
8
+ WatchCallback,
9
+ WatchEventKind,
10
+ SystemError,
11
+ } from "./memory-volume";
12
+ export { ScriptEngine, executeCode } from "./script-engine";
13
+ export type { ModuleRecord, EngineOptions, ResolverFn } from "./script-engine";
14
+ export { spawnEngine, WorkerSandbox, IframeSandbox, spawnProcessWorkerEngine, ProcessWorkerAdapter } from "./engine-factory";
15
+ export type {
16
+ IScriptEngine,
17
+ ExecutionOutcome,
18
+ SpawnEngineConfig,
19
+ EngineConfig,
20
+ VolumeSnapshot,
21
+ } from "./engine-types";
22
+ export {
23
+ generateSandboxDeployment,
24
+ getSandboxPageHtml,
25
+ getSandboxHostingConfig,
26
+ SANDBOX_DEPLOYMENT_GUIDE,
27
+ } from "./isolation-helpers";
28
+ export { buildFileSystemBridge } from "./polyfills/fs";
29
+ export type { FsBridge } from "./polyfills/fs";
30
+ export { buildProcessEnv } from "./polyfills/process";
31
+ export type { ProcessObject, ProcessEnvVars } from "./polyfills/process";
32
+ export * as path from "./polyfills/path";
33
+ export * as http from "./polyfills/http";
34
+ export * as net from "./polyfills/net";
35
+ export * as events from "./polyfills/events";
36
+ export * as stream from "./polyfills/stream";
37
+ export * as url from "./polyfills/url";
38
+ export * as querystring from "./polyfills/querystring";
39
+ export * as util from "./polyfills/util";
40
+ export * as npm from "./packages/installer";
41
+ export { DependencyInstaller, install } from "./packages/installer";
42
+ export { RequestProxy, getProxyInstance, resetProxy } from "./request-proxy";
43
+ export type { ProxyOptions, ServiceWorkerConfig } from "./request-proxy";
44
+ export * as chokidar from "./polyfills/chokidar";
45
+ export * as ws from "./polyfills/ws";
46
+ export * as fsevents from "./polyfills/fsevents";
47
+ export * as readdirp from "./polyfills/readdirp";
48
+ export * as module from "./polyfills/module";
49
+ export * as perf_hooks from "./polyfills/perf_hooks";
50
+ export * as worker_threads from "./polyfills/worker_threads";
51
+ export * as esbuild from "./polyfills/esbuild";
52
+ export * as rollup from "./polyfills/rollup";
53
+ export * as assert from "./polyfills/assert";
54
+
55
+ import { MemoryVolume } from "./memory-volume";
56
+ import { ScriptEngine, EngineOptions } from "./script-engine";
57
+ import { DependencyInstaller } from "./packages/installer";
58
+ import { RequestProxy, getProxyInstance } from "./request-proxy";
59
+ // lazy-load child_process to avoid pulling in the shell at module load time
60
+ let _shellMod: typeof import("./polyfills/child_process") | null = null;
61
+ async function getShellMod() {
62
+ if (!_shellMod) _shellMod = await import("./polyfills/child_process");
63
+ return _shellMod;
64
+ }
65
+
66
+ export interface CommandResult {
67
+ stdout: string;
68
+ stderr: string;
69
+ exitCode: number;
70
+ }
71
+
72
+ export interface CommandOptions {
73
+ cwd?: string;
74
+ onStdout?: (data: string) => void;
75
+ onStderr?: (data: string) => void;
76
+ signal?: AbortSignal;
77
+ }
78
+
79
+ export interface WorkspaceConfig extends EngineOptions {
80
+ baseUrl?: string;
81
+ onServerReady?: (port: number, url: string) => void;
82
+ }
83
+
84
+ // create a fully-wired workspace (volume + engine + packages + proxy)
85
+ export function createWorkspace(config?: WorkspaceConfig): {
86
+ volume: MemoryVolume;
87
+ engine: ScriptEngine;
88
+ packages: DependencyInstaller;
89
+ proxy: RequestProxy;
90
+ execute: (code: string, filename?: string) => { exports: unknown };
91
+ runFile: (filename: string) => { exports: unknown };
92
+ run: (command: string, options?: CommandOptions) => Promise<CommandResult>;
93
+ sendInput: (data: string) => Promise<void>;
94
+ createREPL: () => { eval: (code: string) => unknown };
95
+ on: (event: string, listener: (...args: unknown[]) => void) => void;
96
+ } {
97
+ const volume = new MemoryVolume();
98
+ const engine = new ScriptEngine(volume, config);
99
+ const packages = new DependencyInstaller(volume);
100
+ const proxy = getProxyInstance({
101
+ baseUrl: config?.baseUrl,
102
+ onServerReady: config?.onServerReady,
103
+ });
104
+
105
+ // init shell lazily (SDK path uses Nodepod.boot() instead)
106
+ getShellMod().then((mod) => mod.initShellExec(volume, { cwd: config?.cwd }));
107
+
108
+ return {
109
+ volume,
110
+ engine,
111
+ packages,
112
+ proxy,
113
+ execute: (code: string, filename?: string) =>
114
+ engine.execute(code, filename),
115
+ runFile: (filename: string) => engine.runFile(filename),
116
+ run: async (
117
+ command: string,
118
+ runOpts?: CommandOptions,
119
+ ): Promise<CommandResult> => {
120
+ if (runOpts?.signal?.aborted) {
121
+ return { stdout: "", stderr: "", exitCode: 130 };
122
+ }
123
+
124
+ const shell = await getShellMod();
125
+ const hasStreaming =
126
+ runOpts?.onStdout || runOpts?.onStderr || runOpts?.signal;
127
+ if (hasStreaming) {
128
+ shell.setStreamingCallbacks({
129
+ onStdout: runOpts?.onStdout,
130
+ onStderr: runOpts?.onStderr,
131
+ signal: runOpts?.signal,
132
+ });
133
+ }
134
+
135
+ return new Promise((resolve) => {
136
+ shell.exec(command, { cwd: runOpts?.cwd }, (error, stdout, stderr) => {
137
+ if (hasStreaming) shell.clearStreamingCallbacks();
138
+ resolve({
139
+ stdout: String(stdout),
140
+ stderr: String(stderr),
141
+ exitCode: error ? ((error as any).code ?? 1) : 0,
142
+ });
143
+ });
144
+ });
145
+ },
146
+ sendInput: async (data: string) => {
147
+ const shell = await getShellMod();
148
+ shell.sendStdin(data);
149
+ },
150
+ createREPL: () => engine.createREPL(),
151
+ on: (event: string, listener: (...args: unknown[]) => void) => {
152
+ proxy.on(event, listener);
153
+ },
154
+ };
155
+ }
156
+
157
+ export default createWorkspace;
158
+
159
+ /* ---- SDK (clean public API) ---- */
160
+
161
+ export { Nodepod } from "./sdk/nodepod";
162
+ export { NodepodTerminal } from "./sdk/nodepod-terminal";
163
+ export { NodepodProcess } from "./sdk/nodepod-process";
164
+ export { NodepodFS } from "./sdk/nodepod-fs";
165
+ export type {
166
+ NodepodOptions,
167
+ TerminalOptions,
168
+ TerminalTheme,
169
+ StatResult,
170
+ Snapshot,
171
+ SpawnOptions,
172
+ } from "./sdk/types";
173
+
174
+ /* ---- Threading / Worker Infrastructure ---- */
175
+
176
+ export { ProcessManager } from "./threading/process-manager";
177
+ export { ProcessHandle } from "./threading/process-handle";
178
+ export type { ProcessState } from "./threading/process-handle";
179
+ export { VFSBridge } from "./threading/vfs-bridge";
180
+ export { WorkerVFS } from "./threading/worker-vfs";
181
+ export { SyncChannelController, SyncChannelWorker } from "./threading/sync-channel";
182
+ export { SharedVFSController, SharedVFSReader, isSharedArrayBufferAvailable } from "./threading/shared-vfs";
183
+ export { createProcessContext, getActiveContext, setActiveContext } from "./threading/process-context";
184
+ export type { ProcessContext, ProcessWriter, ProcessReader, OpenFileEntry } from "./threading/process-context";
185
+ export type {
186
+ VFSBinarySnapshot,
187
+ VFSSnapshotEntry,
188
+ SpawnConfig,
189
+ ProcessInfo,
190
+ MainToWorkerMessage,
191
+ WorkerToMainMessage,
192
+ } from "./threading/worker-protocol";