@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.
- package/LICENSE +43 -0
- package/README.md +240 -0
- package/dist/child_process-BJOMsZje.js +8233 -0
- package/dist/child_process-BJOMsZje.js.map +1 -0
- package/dist/child_process-Cj8vOcuc.cjs +7434 -0
- package/dist/child_process-Cj8vOcuc.cjs.map +1 -0
- package/dist/index-Cb1Cgdnd.js +35308 -0
- package/dist/index-Cb1Cgdnd.js.map +1 -0
- package/dist/index-DsMGS-xc.cjs +37195 -0
- package/dist/index-DsMGS-xc.cjs.map +1 -0
- package/dist/index.cjs +65 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +59 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +95 -0
- package/src/__tests__/smoke.test.ts +11 -0
- package/src/constants/cdn-urls.ts +18 -0
- package/src/constants/config.ts +236 -0
- package/src/cross-origin.ts +26 -0
- package/src/engine-factory.ts +176 -0
- package/src/engine-types.ts +56 -0
- package/src/helpers/byte-encoding.ts +39 -0
- package/src/helpers/digest.ts +9 -0
- package/src/helpers/event-loop.ts +96 -0
- package/src/helpers/wasm-cache.ts +133 -0
- package/src/iframe-sandbox.ts +141 -0
- package/src/index.ts +192 -0
- package/src/isolation-helpers.ts +148 -0
- package/src/memory-volume.ts +941 -0
- package/src/module-transformer.ts +368 -0
- package/src/packages/archive-extractor.ts +248 -0
- package/src/packages/browser-bundler.ts +284 -0
- package/src/packages/installer.ts +396 -0
- package/src/packages/registry-client.ts +131 -0
- package/src/packages/version-resolver.ts +411 -0
- package/src/polyfills/assert.ts +384 -0
- package/src/polyfills/async_hooks.ts +144 -0
- package/src/polyfills/buffer.ts +628 -0
- package/src/polyfills/child_process.ts +2288 -0
- package/src/polyfills/chokidar.ts +336 -0
- package/src/polyfills/cluster.ts +106 -0
- package/src/polyfills/console.ts +136 -0
- package/src/polyfills/constants.ts +123 -0
- package/src/polyfills/crypto.ts +885 -0
- package/src/polyfills/dgram.ts +87 -0
- package/src/polyfills/diagnostics_channel.ts +76 -0
- package/src/polyfills/dns.ts +134 -0
- package/src/polyfills/domain.ts +68 -0
- package/src/polyfills/esbuild.ts +854 -0
- package/src/polyfills/events.ts +276 -0
- package/src/polyfills/fs.ts +2888 -0
- package/src/polyfills/fsevents.ts +79 -0
- package/src/polyfills/http.ts +1449 -0
- package/src/polyfills/http2.ts +199 -0
- package/src/polyfills/https.ts +76 -0
- package/src/polyfills/inspector.ts +62 -0
- package/src/polyfills/lightningcss.ts +105 -0
- package/src/polyfills/module.ts +191 -0
- package/src/polyfills/net.ts +353 -0
- package/src/polyfills/os.ts +238 -0
- package/src/polyfills/path.ts +206 -0
- package/src/polyfills/perf_hooks.ts +102 -0
- package/src/polyfills/process.ts +690 -0
- package/src/polyfills/punycode.ts +159 -0
- package/src/polyfills/querystring.ts +93 -0
- package/src/polyfills/quic.ts +118 -0
- package/src/polyfills/readdirp.ts +229 -0
- package/src/polyfills/readline.ts +692 -0
- package/src/polyfills/repl.ts +134 -0
- package/src/polyfills/rollup.ts +119 -0
- package/src/polyfills/sea.ts +33 -0
- package/src/polyfills/sqlite.ts +78 -0
- package/src/polyfills/stream.ts +1620 -0
- package/src/polyfills/string_decoder.ts +25 -0
- package/src/polyfills/tailwindcss-oxide.ts +309 -0
- package/src/polyfills/test.ts +197 -0
- package/src/polyfills/timers.ts +32 -0
- package/src/polyfills/tls.ts +105 -0
- package/src/polyfills/trace_events.ts +50 -0
- package/src/polyfills/tty.ts +71 -0
- package/src/polyfills/url.ts +174 -0
- package/src/polyfills/util.ts +559 -0
- package/src/polyfills/v8.ts +126 -0
- package/src/polyfills/vm.ts +132 -0
- package/src/polyfills/volume-registry.ts +15 -0
- package/src/polyfills/wasi.ts +44 -0
- package/src/polyfills/worker_threads.ts +326 -0
- package/src/polyfills/ws.ts +595 -0
- package/src/polyfills/zlib.ts +881 -0
- package/src/request-proxy.ts +716 -0
- package/src/script-engine.ts +3375 -0
- package/src/sdk/nodepod-fs.ts +93 -0
- package/src/sdk/nodepod-process.ts +86 -0
- package/src/sdk/nodepod-terminal.ts +350 -0
- package/src/sdk/nodepod.ts +509 -0
- package/src/sdk/types.ts +70 -0
- package/src/shell/commands/bun.ts +121 -0
- package/src/shell/commands/directory.ts +297 -0
- package/src/shell/commands/file-ops.ts +525 -0
- package/src/shell/commands/git.ts +2142 -0
- package/src/shell/commands/node.ts +80 -0
- package/src/shell/commands/npm.ts +198 -0
- package/src/shell/commands/pm-types.ts +45 -0
- package/src/shell/commands/pnpm.ts +82 -0
- package/src/shell/commands/search.ts +264 -0
- package/src/shell/commands/shell-env.ts +352 -0
- package/src/shell/commands/text-processing.ts +1152 -0
- package/src/shell/commands/yarn.ts +84 -0
- package/src/shell/shell-builtins.ts +19 -0
- package/src/shell/shell-helpers.ts +250 -0
- package/src/shell/shell-interpreter.ts +514 -0
- package/src/shell/shell-parser.ts +429 -0
- package/src/shell/shell-types.ts +85 -0
- package/src/syntax-transforms.ts +561 -0
- package/src/threading/engine-worker.ts +64 -0
- package/src/threading/inline-worker.ts +372 -0
- package/src/threading/offload-types.ts +112 -0
- package/src/threading/offload-worker.ts +383 -0
- package/src/threading/offload.ts +271 -0
- package/src/threading/process-context.ts +92 -0
- package/src/threading/process-handle.ts +275 -0
- package/src/threading/process-manager.ts +956 -0
- package/src/threading/process-worker-entry.ts +854 -0
- package/src/threading/shared-vfs.ts +352 -0
- package/src/threading/sync-channel.ts +135 -0
- package/src/threading/task-queue.ts +177 -0
- package/src/threading/vfs-bridge.ts +231 -0
- package/src/threading/worker-pool.ts +233 -0
- package/src/threading/worker-protocol.ts +358 -0
- package/src/threading/worker-vfs.ts +218 -0
- package/src/types/externals.d.ts +38 -0
- package/src/types/fs-streams.ts +142 -0
- package/src/types/manifest.ts +17 -0
- 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
|
+
}
|
package/src/sdk/types.ts
ADDED
|
@@ -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
|
+
}
|