sandboxedjs 0.1.29 → 0.1.31

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.
@@ -0,0 +1,112 @@
1
+ // src/preview/router.ts
2
+ var CLAIM = /(?:^|\/)__sbx__\/(\d+)(?:\/|$)/;
3
+ var NOT_CONNECTED = 503;
4
+ var PreviewRouter = class {
5
+ constructor(options) {
6
+ this.options = options;
7
+ }
8
+ options;
9
+ clientPorts = /* @__PURE__ */ new Map();
10
+ pending = /* @__PURE__ */ new Map();
11
+ nextId = 1;
12
+ connected = false;
13
+ /** The page that answers requests has arrived. */
14
+ connect() {
15
+ this.connected = true;
16
+ }
17
+ /** Read a port out of a claim path, or null when this is not one. */
18
+ claimedPort(pathname) {
19
+ const match = CLAIM.exec(pathname);
20
+ return match ? Number(match[1]) : null;
21
+ }
22
+ /** Remember that everything from `clientId` belongs to `port`. */
23
+ bind(clientId, port) {
24
+ if (clientId) this.clientPorts.set(clientId, port);
25
+ }
26
+ /** Which port this client is previewing, or undefined for ordinary traffic. */
27
+ portFor(clientId) {
28
+ return this.clientPorts.get(clientId);
29
+ }
30
+ /** A response has come back from the page. */
31
+ settle(id, response) {
32
+ const resolve = this.pending.get(id);
33
+ if (!resolve) return;
34
+ this.pending.delete(id);
35
+ resolve(response);
36
+ }
37
+ /** Forget a client that has gone away. */
38
+ release(clientId) {
39
+ this.clientPorts.delete(clientId);
40
+ }
41
+ /** Ask the page for one response and rebuild it as a `Response`. */
42
+ async fetch(port, path, request) {
43
+ if (!this.connected) {
44
+ return new Response("The sandbox preview is not connected yet.", { status: NOT_CONNECTED });
45
+ }
46
+ const body = request.method === "GET" || request.method === "HEAD" ? void 0 : await request.arrayBuffer();
47
+ const id = this.nextId++;
48
+ const answered = new Promise((resolve) => this.pending.set(id, resolve));
49
+ this.options.send(
50
+ { id, port, path, method: request.method, headers: headerRecord(request.headers), ...body ? { body } : {} },
51
+ body ? [body] : []
52
+ );
53
+ const timeoutMs = this.options.timeoutMs ?? 3e4;
54
+ const result = await Promise.race([
55
+ answered,
56
+ new Promise(
57
+ (resolve) => setTimeout(
58
+ () => resolve({ status: 504, statusText: "Gateway Timeout", headers: {}, body: new ArrayBuffer(0) }),
59
+ timeoutMs
60
+ )
61
+ )
62
+ ]);
63
+ this.pending.delete(id);
64
+ const headers = new Headers(result.headers);
65
+ headers.set("Cross-Origin-Resource-Policy", "cross-origin");
66
+ headers.set("Cross-Origin-Embedder-Policy", "require-corp");
67
+ return new Response(result.body, { status: result.status, statusText: result.statusText, headers });
68
+ }
69
+ };
70
+ function headerRecord(headers) {
71
+ const out = {};
72
+ headers.forEach((value, key) => {
73
+ out[key] = value;
74
+ });
75
+ return out;
76
+ }
77
+
78
+ // src/preview/service-worker.ts
79
+ var worker = globalThis;
80
+ var host = null;
81
+ var router = new PreviewRouter({
82
+ send: (request, transfer) => host?.postMessage(request, transfer)
83
+ });
84
+ worker.addEventListener("install", () => void worker.skipWaiting());
85
+ worker.addEventListener("activate", (event) => event.waitUntil(worker.clients.claim()));
86
+ worker.addEventListener("message", (event) => {
87
+ const data = event.data;
88
+ if (data?.type !== "sandboxedjs:connect") return;
89
+ host = event.ports[0] ?? null;
90
+ if (!host) return;
91
+ host.onmessage = (message) => {
92
+ const reply = message.data;
93
+ router.settle(reply.id, reply.response);
94
+ };
95
+ host.start();
96
+ router.connect();
97
+ });
98
+ worker.addEventListener("fetch", (event) => {
99
+ const url = new URL(event.request.url);
100
+ if (url.origin !== worker.location.origin) return;
101
+ const claimed = router.claimedPort(url.pathname);
102
+ if (claimed !== null && event.request.mode === "navigate") {
103
+ router.bind(event.resultingClientId || event.clientId, claimed);
104
+ event.respondWith(router.fetch(claimed, "/", event.request));
105
+ return;
106
+ }
107
+ const port = router.portFor(event.clientId);
108
+ if (port === void 0) return;
109
+ event.respondWith(router.fetch(port, url.pathname + url.search, event.request));
110
+ });
111
+ //# sourceMappingURL=service-worker.js.map
112
+ //# sourceMappingURL=service-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/preview/router.ts","../src/preview/service-worker.ts"],"names":[],"mappings":"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","file":"service-worker.js","sourcesContent":["/**\n * The routing a preview service worker does, separated from the worker itself.\n *\n * A service worker is awkward to test — it needs a browser, a registration and\n * a secure context — but almost none of what makes this correct is about being\n * a service worker. The interesting part is the bookkeeping: which client is\n * previewing which port, which requests belong to a preview at all, and how a\n * response is rebuilt on the way back. That lives here, where it can be\n * exercised directly.\n *\n * The rule this implements, and the reason it exists: requests are routed by\n * **who is asking**, not by what the path looks like. An iframe navigates once\n * to `/__sbx__/<port>/`; every later request from that client — however\n * absolute its path — is recognised by its client id. Nothing has to rewrite\n * `/src/main.js` or `/@vite/client`, which is what makes a real dev server\n * work rather than only a single page.\n */\n\n/**\n * The one-time path an iframe navigates to in order to claim a port.\n *\n * Deliberately not anchored to the start of the path. A service worker's scope\n * is the directory its script was served from, so in a published package the\n * claim arrives under something like\n * `/node_modules/sandboxedjs/dist/__sbx__/5173/` — anchoring here would match\n * only when the worker happened to be served from the site root.\n */\nconst CLAIM = /(?:^|\\/)__sbx__\\/(\\d+)(?:\\/|$)/;\n\nexport interface ProxiedRequest {\n id: number;\n port: number;\n path: string;\n method: string;\n headers: Record<string, string>;\n body?: ArrayBuffer;\n}\n\nexport interface ProxiedResponse {\n status: number;\n statusText: string;\n headers: Record<string, string>;\n body: ArrayBuffer;\n}\n\nexport interface PreviewRouterOptions {\n /** Hand a request to whichever page owns the container. */\n send(request: ProxiedRequest, transfer: ArrayBuffer[]): void;\n /** How long to wait for that page before giving up. */\n timeoutMs?: number;\n}\n\n/** The port to answer with when nothing has connected yet. */\nconst NOT_CONNECTED = 503;\n\nexport class PreviewRouter {\n private readonly clientPorts = new Map<string, number>();\n private readonly pending = new Map<number, (value: ProxiedResponse) => void>();\n private nextId = 1;\n private connected = false;\n\n constructor(private readonly options: PreviewRouterOptions) {}\n\n /** The page that answers requests has arrived. */\n connect(): void {\n this.connected = true;\n }\n\n /** Read a port out of a claim path, or null when this is not one. */\n claimedPort(pathname: string): number | null {\n const match = CLAIM.exec(pathname);\n return match ? Number(match[1]) : null;\n }\n\n /** Remember that everything from `clientId` belongs to `port`. */\n bind(clientId: string, port: number): void {\n if (clientId) this.clientPorts.set(clientId, port);\n }\n\n /** Which port this client is previewing, or undefined for ordinary traffic. */\n portFor(clientId: string): number | undefined {\n return this.clientPorts.get(clientId);\n }\n\n /** A response has come back from the page. */\n settle(id: number, response: ProxiedResponse): void {\n const resolve = this.pending.get(id);\n if (!resolve) return;\n this.pending.delete(id);\n resolve(response);\n }\n\n /** Forget a client that has gone away. */\n release(clientId: string): void {\n this.clientPorts.delete(clientId);\n }\n\n /** Ask the page for one response and rebuild it as a `Response`. */\n async fetch(port: number, path: string, request: Request): Promise<Response> {\n if (!this.connected) {\n return new Response(\"The sandbox preview is not connected yet.\", { status: NOT_CONNECTED });\n }\n const body = request.method === \"GET\" || request.method === \"HEAD\" ? undefined : await request.arrayBuffer();\n const id = this.nextId++;\n const answered = new Promise<ProxiedResponse>((resolve) => this.pending.set(id, resolve));\n\n this.options.send(\n { id, port, path, method: request.method, headers: headerRecord(request.headers), ...(body ? { body } : {}) },\n body ? [body] : [],\n );\n\n const timeoutMs = this.options.timeoutMs ?? 30_000;\n const result = await Promise.race([\n answered,\n new Promise<ProxiedResponse>((resolve) =>\n setTimeout(\n () => resolve({ status: 504, statusText: \"Gateway Timeout\", headers: {}, body: new ArrayBuffer(0) }),\n timeoutMs,\n ),\n ),\n ]);\n this.pending.delete(id);\n\n const headers = new Headers(result.headers);\n /*\n * Two headers, for two different refusals, both of which a cross-origin\n * isolated page makes — and pages that run Rolldown have to be isolated.\n *\n * `Cross-Origin-Resource-Policy` lets the response be loaded as a\n * subresource at all. `Cross-Origin-Embedder-Policy` lets it be *framed*:\n * without it the document is not isolation-compatible, so the iframe is\n * replaced by an error page and the parent sees an opaque, cross-origin\n * frame rather than the site it asked for. Everything served here carries\n * CORP, so the stricter policy is one this preview can satisfy.\n */\n headers.set(\"Cross-Origin-Resource-Policy\", \"cross-origin\");\n headers.set(\"Cross-Origin-Embedder-Policy\", \"require-corp\");\n return new Response(result.body, { status: result.status, statusText: result.statusText, headers });\n }\n}\n\n/** `Headers` is iterable at runtime; the DOM lib used here does not say so. */\nfunction headerRecord(headers: Headers): Record<string, string> {\n const out: Record<string, string> = {};\n (headers as unknown as { forEach(fn: (value: string, key: string) => void): void })\n .forEach((value, key) => { out[key] = value; });\n return out;\n}\n","/**\n * A service worker that gives a container's HTTP servers real URLs.\n *\n * Without one, a page can reach a server inside the container only through\n * `box.request(port, …)`, which hands back a single response. That is enough to\n * read an API or drop some HTML into an element, but not to *run* a site: a\n * dev server emits absolute URLs like `/src/main.js` and `/@vite/client`, and\n * nothing in the page resolves those to the sandbox.\n *\n * This file is deliberately only the glue. The routing lives in\n * {@link PreviewRouter}, where it can be tested without a browser — a service\n * worker otherwise needs a registration, a secure context and a real page\n * before any of its behaviour can be observed at all.\n */\n\nimport { PreviewRouter, type ProxiedResponse } from \"./router.js\";\n\n/*\n * Typed locally rather than through `lib.webworker`: this package compiles with\n * the DOM lib, and the two declare a conflicting `self`. Only the handful of\n * members actually used are described here.\n */\ninterface FetchLikeEvent extends Event {\n request: Request;\n clientId: string;\n resultingClientId: string;\n respondWith(response: Promise<Response> | Response): void;\n}\ninterface ExtendableLikeEvent extends Event {\n waitUntil(promise: Promise<unknown>): void;\n}\ninterface MessageLikeEvent extends Event {\n data: unknown;\n ports: readonly MessagePort[];\n}\ninterface WorkerScope {\n location: { origin: string };\n clients: { claim(): Promise<void> };\n skipWaiting(): Promise<void>;\n addEventListener(type: \"install\" | \"activate\", listener: (event: ExtendableLikeEvent) => void): void;\n addEventListener(type: \"fetch\", listener: (event: FetchLikeEvent) => void): void;\n addEventListener(type: \"message\", listener: (event: MessageLikeEvent) => void): void;\n}\n\nconst worker = globalThis as unknown as WorkerScope;\n\nlet host: MessagePort | null = null;\nconst router = new PreviewRouter({\n send: (request, transfer) => host?.postMessage(request, transfer),\n});\n\nworker.addEventListener(\"install\", () => void worker.skipWaiting());\nworker.addEventListener(\"activate\", (event) => event.waitUntil(worker.clients.claim()));\n\nworker.addEventListener(\"message\", (event) => {\n const data = event.data as { type?: string } | null;\n if (data?.type !== \"sandboxedjs:connect\") return;\n host = event.ports[0] ?? null;\n if (!host) return;\n host.onmessage = (message) => {\n const reply = message.data as { id: number; response: ProxiedResponse };\n router.settle(reply.id, reply.response);\n };\n host.start();\n router.connect();\n});\n\nworker.addEventListener(\"fetch\", (event) => {\n const url = new URL(event.request.url);\n if (url.origin !== worker.location.origin) return;\n\n const claimed = router.claimedPort(url.pathname);\n if (claimed !== null && event.request.mode === \"navigate\") {\n /* A navigation's client does not exist yet, so it is named by\n * `resultingClientId`. Binding it here is what lets every later request\n * from that document be recognised without touching its URLs. */\n router.bind(event.resultingClientId || event.clientId, claimed);\n event.respondWith(router.fetch(claimed, \"/\", event.request));\n return;\n }\n\n const port = router.portFor(event.clientId);\n if (port === undefined) return; // not a preview: the host page's own traffic\n event.respondWith(router.fetch(port, url.pathname + url.search, event.request));\n});\n"]}
@@ -27251,6 +27251,7 @@ var VirtualHttpServer = class extends import_events.default {
27251
27251
  owner;
27252
27252
  listening = false;
27253
27253
  portValue = null;
27254
+ referenced = true;
27254
27255
  listen(...args) {
27255
27256
  const callback = typeof args.at(-1) === "function" ? args.pop() : void 0;
27256
27257
  const first = args[0];
@@ -27279,11 +27280,16 @@ var VirtualHttpServer = class extends import_events.default {
27279
27280
  return this.portValue === null ? null : { address: "0.0.0.0", family: "IPv4", port: this.portValue };
27280
27281
  }
27281
27282
  ref() {
27283
+ this.referenced = true;
27282
27284
  return this;
27283
27285
  }
27284
27286
  unref() {
27287
+ this.referenced = false;
27285
27288
  return this;
27286
27289
  }
27290
+ hasRef() {
27291
+ return this.referenced;
27292
+ }
27287
27293
  setTimeout(_milliseconds, callback) {
27288
27294
  if (callback) this.on("timeout", callback);
27289
27295
  return this;
@@ -27293,13 +27299,16 @@ var VirtualHttpRouter = class {
27293
27299
  servers = /* @__PURE__ */ new Map();
27294
27300
  /** Notified when a server begins listening, for `onServerReady`. */
27295
27301
  onListen;
27302
+ onClose;
27296
27303
  register(port, server, owner) {
27297
27304
  if (this.servers.has(port)) throw Object.assign(new Error(`listen EADDRINUSE: address already in use 0.0.0.0:${port}`), { code: "EADDRINUSE", port });
27298
27305
  this.servers.set(port, { server, owner });
27299
27306
  this.onListen?.(port);
27300
27307
  }
27301
27308
  unregister(port, server) {
27302
- if (this.servers.get(port)?.server === server) this.servers.delete(port);
27309
+ if (this.servers.get(port)?.server !== server) return;
27310
+ this.servers.delete(port);
27311
+ this.onClose?.(port);
27303
27312
  }
27304
27313
  /** Whether anything in this container is listening on `port`. */
27305
27314
  activePortsIncludes(port) {
@@ -27308,6 +27317,9 @@ var VirtualHttpRouter = class {
27308
27317
  activePorts(owner) {
27309
27318
  return [...this.servers].filter(([, item]) => owner === void 0 || item.owner === owner).map(([port]) => port).sort((a, b) => a - b);
27310
27319
  }
27320
+ referencedPorts(owner) {
27321
+ return [...this.servers].filter(([, item]) => item.owner === owner && item.server.hasRef()).map(([port]) => port);
27322
+ }
27311
27323
  closeOwner(owner) {
27312
27324
  for (const { server, owner: value } of [...this.servers.values()]) if (value === owner) server.close();
27313
27325
  }
@@ -28582,6 +28594,102 @@ function toBytes2(data2, encoding) {
28582
28594
  return typeof data2 === "string" ? Buffer2.from(data2, encoding) : data2;
28583
28595
  }
28584
28596
 
28597
+ // src/runtime/sync-channel.ts
28598
+ var STATE = 0;
28599
+ var LENGTH2 = 1;
28600
+ var MORE = 2;
28601
+ var STATE_IDLE = 0;
28602
+ var STATE_REQUEST = 1;
28603
+ var STATE_RESPONSE = 2;
28604
+ var STATE_CONTINUE = 3;
28605
+ var WAIT_SLICE_MS = 250;
28606
+ function syncChannelUnavailableReason() {
28607
+ if (typeof globalThis.crossOriginIsolated === "boolean" && !globalThis.crossOriginIsolated) {
28608
+ return "The browser host is not cross-origin isolated. Serve the host app over HTTPS or localhost with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp. For a built app use sandboxedjs-serve <directory>. Embedded browsers or iframe policies may also prevent isolation.";
28609
+ }
28610
+ if (typeof SharedArrayBuffer !== "function") return "SharedArrayBuffer is unavailable in this host.";
28611
+ if (typeof Atomics !== "object" || typeof Atomics.wait !== "function") return "Atomics.wait is unavailable in this host.";
28612
+ return null;
28613
+ }
28614
+ var SyncChannelClosedError = class extends Error {
28615
+ code = "ERR_SYNC_CHANNEL_CLOSED";
28616
+ constructor() {
28617
+ super("the sandbox runtime host went away while a synchronous call was waiting");
28618
+ }
28619
+ };
28620
+ var SyncChannelClient = class {
28621
+ constructor(buffers, wake) {
28622
+ this.wake = wake;
28623
+ this.control = new Int32Array(buffers.control);
28624
+ this.data = new Uint8Array(buffers.data);
28625
+ this.capacity = this.data.length;
28626
+ }
28627
+ wake;
28628
+ control;
28629
+ data;
28630
+ capacity;
28631
+ /** Send a request and block until the whole response has come back. */
28632
+ call(request) {
28633
+ this.send(request);
28634
+ return this.receive();
28635
+ }
28636
+ /** Write the request across, a window at a time. */
28637
+ send(request) {
28638
+ let offset2 = 0;
28639
+ for (; ; ) {
28640
+ const size = Math.min(this.capacity, request.length - offset2);
28641
+ this.data.set(request.subarray(offset2, offset2 + size), 0);
28642
+ offset2 += size;
28643
+ Atomics.store(this.control, LENGTH2, size);
28644
+ Atomics.store(this.control, MORE, offset2 < request.length ? 1 : 0);
28645
+ this.publish(STATE_REQUEST);
28646
+ const next = this.waitWhile(STATE_REQUEST);
28647
+ if (next === STATE_RESPONSE) return;
28648
+ if (next !== STATE_CONTINUE) throw new SyncChannelClosedError();
28649
+ }
28650
+ }
28651
+ /** Read the response back, a window at a time. */
28652
+ receive() {
28653
+ const parts = [];
28654
+ let total = 0;
28655
+ for (; ; ) {
28656
+ const size = Atomics.load(this.control, LENGTH2);
28657
+ parts.push(this.data.slice(0, size));
28658
+ total += size;
28659
+ if (Atomics.load(this.control, MORE) === 0) break;
28660
+ this.publish(STATE_CONTINUE);
28661
+ if (this.waitWhile(STATE_CONTINUE) !== STATE_RESPONSE) throw new SyncChannelClosedError();
28662
+ }
28663
+ Atomics.store(this.control, STATE, STATE_IDLE);
28664
+ if (parts.length === 1) return parts[0];
28665
+ const joined = new Uint8Array(total);
28666
+ let at2 = 0;
28667
+ for (const part of parts) {
28668
+ joined.set(part, at2);
28669
+ at2 += part.length;
28670
+ }
28671
+ return joined;
28672
+ }
28673
+ publish(state) {
28674
+ Atomics.store(this.control, STATE, state);
28675
+ Atomics.notify(this.control, STATE);
28676
+ this.wake();
28677
+ }
28678
+ /**
28679
+ * Block until the state moves off `state`.
28680
+ *
28681
+ * Waking periodically rather than waiting forever is what lets a torn-down
28682
+ * host release a worker instead of stranding it.
28683
+ */
28684
+ waitWhile(state) {
28685
+ for (; ; ) {
28686
+ Atomics.wait(this.control, STATE, state, WAIT_SLICE_MS);
28687
+ const current2 = Atomics.load(this.control, STATE);
28688
+ if (current2 !== state) return current2;
28689
+ }
28690
+ }
28691
+ };
28692
+
28585
28693
  // src/runtime/child-process-module.ts
28586
28694
  var import_events2 = __toESM(require_events());
28587
28695
  var import_stream_browserify3 = __toESM(require_stream_browserify());
@@ -28657,7 +28765,8 @@ var ChildProcess = class extends import_events2.default {
28657
28765
  queueMicrotask(() => this.emit("close", code, null));
28658
28766
  }
28659
28767
  };
28660
- function createChildProcessModule(spawnChild, defaultCwd, syncSpawn) {
28768
+ function createChildProcessModule(spawnChild, defaultCwd, syncSpawn, defaultEnv = () => ({})) {
28769
+ const environmentFor = (options) => options.env ? { ...options.env } : defaultEnv();
28661
28770
  const throughShell = (command, options) => {
28662
28771
  const shell = typeof options.shell === "string" ? options.shell : "/bin/sh";
28663
28772
  return { file: shell, args: ["-c", command] };
@@ -28668,7 +28777,7 @@ function createChildProcessModule(spawnChild, defaultCwd, syncSpawn) {
28668
28777
  command: resolved.file,
28669
28778
  args: resolved.args,
28670
28779
  cwd: options.cwd ?? defaultCwd(),
28671
- ...options.env ? { env: options.env } : {}
28780
+ env: environmentFor(options)
28672
28781
  });
28673
28782
  return new ChildProcess(handle, resolved.file, resolved.args);
28674
28783
  };
@@ -28714,11 +28823,11 @@ ${err2.join("")}`),
28714
28823
  exec,
28715
28824
  execFile,
28716
28825
  fork: (modulePath, args = [], options = {}) => spawn("node", [modulePath, ...args], options),
28717
- ...buildSyncFamily(syncSpawn, throughShell, defaultCwd),
28826
+ ...buildSyncFamily(syncSpawn, throughShell, defaultCwd, environmentFor),
28718
28827
  ChildProcess
28719
28828
  };
28720
28829
  }
28721
- function buildSyncFamily(syncSpawn, throughShell, defaultCwd) {
28830
+ function buildSyncFamily(syncSpawn, throughShell, defaultCwd, environmentFor) {
28722
28831
  if (!syncSpawn) {
28723
28832
  return {
28724
28833
  execSync: unavailable("execSync"),
@@ -28729,12 +28838,14 @@ function buildSyncFamily(syncSpawn, throughShell, defaultCwd) {
28729
28838
  const run2 = (file, args, options) => {
28730
28839
  const resolved = options.shell ? throughShell([file, ...args].join(" "), options) : { file, args };
28731
28840
  const input = options.input === void 0 ? void 0 : typeof options.input === "string" ? options.input : new TextDecoder().decode(options.input);
28841
+ const inherit = options.stdio === "inherit" || Array.isArray(options.stdio) && options.stdio[0] === "inherit";
28732
28842
  return syncSpawn({
28733
28843
  command: resolved.file,
28734
28844
  args: resolved.args,
28735
28845
  cwd: options.cwd ?? defaultCwd(),
28736
- ...options.env ? { env: options.env } : {},
28737
- ...input === void 0 ? {} : { input }
28846
+ env: environmentFor(options),
28847
+ ...input === void 0 ? {} : { input },
28848
+ ...inherit ? { inheritStdio: true } : {}
28738
28849
  });
28739
28850
  };
28740
28851
  const asOutput = (text, options) => options.encoding === "buffer" || options.encoding === void 0 ? Buffer2.from(text) : text;
@@ -28786,7 +28897,7 @@ function unavailable(name) {
28786
28897
  const list = Array.isArray(args[1]) ? args[1].map(String) : [];
28787
28898
  const command = file ? [file, ...list].join(" ") : void 0;
28788
28899
  const error = new Error(
28789
- `child_process.${name} is not supported in the sandboxed runtime` + (command ? ` (tried to run: ${command})` : "") + `: the caller, the child and the event loop share one thread, so blocking the caller would also stop the child. Run it with the asynchronous form (spawn/exec/execFile), or run the command from the container shell.`
28900
+ `child_process.${name} is not supported in the sandboxed runtime` + (command ? ` (tried to run: ${command})` : "") + `: the caller, the child and the event loop share one thread, so blocking the caller would also stop the child. Enable the SandboxedJs worker runtime with createContainer({ isolation: "worker" }) and fix any boot prerequisite it reports. ` + (syncChannelUnavailableReason() ?? "This process is using the realm runtime, which cannot provide synchronous child processes.")
28790
28901
  );
28791
28902
  error.code = "ERR_FEATURE_UNAVAILABLE_ON_PLATFORM";
28792
28903
  throw error;
@@ -29112,6 +29223,11 @@ function createReadlineModule(defaultInput, defaultOutput = () => void 0) {
29112
29223
  edit(sequence, key) {
29113
29224
  if (this.closed) return;
29114
29225
  const name = key.name;
29226
+ if (key.ctrl && name === "c") {
29227
+ if (this.listenerCount("SIGINT") > 0) this.emit("SIGINT");
29228
+ else this.close();
29229
+ return;
29230
+ }
29115
29231
  if (name === "return" || name === "enter") {
29116
29232
  const value = this.line;
29117
29233
  this.line = "";
@@ -29207,7 +29323,7 @@ function createReadlineModule(defaultInput, defaultOutput = () => void 0) {
29207
29323
  };
29208
29324
  const createInterface = (options, output) => {
29209
29325
  if (options && typeof options === "object" && !options.on) {
29210
- const resolvedOutput2 = options.output ?? defaultOutput();
29326
+ const resolvedOutput2 = options.output;
29211
29327
  const instance = new Interface(
29212
29328
  options.input ?? defaultInput(),
29213
29329
  resolvedOutput2,
@@ -29216,7 +29332,7 @@ function createReadlineModule(defaultInput, defaultOutput = () => void 0) {
29216
29332
  if (typeof options.prompt === "string") instance.setPrompt(options.prompt);
29217
29333
  return instance;
29218
29334
  }
29219
- const resolvedOutput = output ?? defaultOutput();
29335
+ const resolvedOutput = output;
29220
29336
  return new Interface(options ?? defaultInput(), resolvedOutput, isTerminal(void 0, resolvedOutput));
29221
29337
  };
29222
29338
  const noop = () => {
@@ -29456,7 +29572,7 @@ function createCoreModules(options) {
29456
29572
  };
29457
29573
  const http = options.http ? createHttpModule(options.http.router, options.http.owner, httpOptions, "http:") : createUnsupportedModule("http");
29458
29574
  const https = options.http ? createHttpModule(options.http.router, options.http.owner, httpOptions, "https:") : createUnsupportedModule("https");
29459
- const childProcess = options.spawnChild ? createChildProcessModule(options.spawnChild, () => cwd, options.syncSpawn) : createUnsupportedModule("child_process");
29575
+ const childProcess = options.spawnChild ? createChildProcessModule(options.spawnChild, () => cwd, options.syncSpawn, () => ({ ...processObject.env })) : createUnsupportedModule("child_process");
29460
29576
  const readline = createReadlineModule(() => processObject.stdin, () => processObject.stdout);
29461
29577
  const dns = createDnsModule();
29462
29578
  const builtins = {
@@ -29489,7 +29605,7 @@ function createCoreModules(options) {
29489
29605
  "stream/promises": createStreamPromises(),
29490
29606
  string_decoder: import_string_decoder.default,
29491
29607
  timers: { ...import_timers_browserify.default, ...timers.api },
29492
- "timers/promises": createTimerPromises(),
29608
+ "timers/promises": createTimerPromises(timers.api),
29493
29609
  tty: { isatty: () => false, ReadStream: import_stream_browserify4.default.Readable, WriteStream: import_stream_browserify4.default.Writable },
29494
29610
  url: url_module_default,
29495
29611
  util: util_module_default,
@@ -30234,10 +30350,10 @@ function createTrackedTimers() {
30234
30350
  cancelAll
30235
30351
  };
30236
30352
  }
30237
- function createTimerPromises() {
30353
+ function createTimerPromises(timers) {
30238
30354
  return {
30239
- setTimeout: (delay, value) => new Promise((resolve2) => setTimeout(resolve2, delay, value)),
30240
- setImmediate: (value) => new Promise((resolve2) => setTimeout(resolve2, 0, value))
30355
+ setTimeout: (delay, value) => new Promise((resolve2) => timers.setTimeout(resolve2, delay, value)),
30356
+ setImmediate: (value) => new Promise((resolve2) => timers.setImmediate(resolve2, value))
30241
30357
  };
30242
30358
  }
30243
30359
  function createAsyncHooksModule() {
@@ -30488,94 +30604,6 @@ var RemoteVolume = class {
30488
30604
  }
30489
30605
  };
30490
30606
 
30491
- // src/runtime/sync-channel.ts
30492
- var STATE = 0;
30493
- var LENGTH2 = 1;
30494
- var MORE = 2;
30495
- var STATE_IDLE = 0;
30496
- var STATE_REQUEST = 1;
30497
- var STATE_RESPONSE = 2;
30498
- var STATE_CONTINUE = 3;
30499
- var WAIT_SLICE_MS = 250;
30500
- var SyncChannelClosedError = class extends Error {
30501
- code = "ERR_SYNC_CHANNEL_CLOSED";
30502
- constructor() {
30503
- super("the sandbox runtime host went away while a synchronous call was waiting");
30504
- }
30505
- };
30506
- var SyncChannelClient = class {
30507
- constructor(buffers, wake) {
30508
- this.wake = wake;
30509
- this.control = new Int32Array(buffers.control);
30510
- this.data = new Uint8Array(buffers.data);
30511
- this.capacity = this.data.length;
30512
- }
30513
- wake;
30514
- control;
30515
- data;
30516
- capacity;
30517
- /** Send a request and block until the whole response has come back. */
30518
- call(request) {
30519
- this.send(request);
30520
- return this.receive();
30521
- }
30522
- /** Write the request across, a window at a time. */
30523
- send(request) {
30524
- let offset2 = 0;
30525
- for (; ; ) {
30526
- const size = Math.min(this.capacity, request.length - offset2);
30527
- this.data.set(request.subarray(offset2, offset2 + size), 0);
30528
- offset2 += size;
30529
- Atomics.store(this.control, LENGTH2, size);
30530
- Atomics.store(this.control, MORE, offset2 < request.length ? 1 : 0);
30531
- this.publish(STATE_REQUEST);
30532
- const next = this.waitWhile(STATE_REQUEST);
30533
- if (next === STATE_RESPONSE) return;
30534
- if (next !== STATE_CONTINUE) throw new SyncChannelClosedError();
30535
- }
30536
- }
30537
- /** Read the response back, a window at a time. */
30538
- receive() {
30539
- const parts = [];
30540
- let total = 0;
30541
- for (; ; ) {
30542
- const size = Atomics.load(this.control, LENGTH2);
30543
- parts.push(this.data.slice(0, size));
30544
- total += size;
30545
- if (Atomics.load(this.control, MORE) === 0) break;
30546
- this.publish(STATE_CONTINUE);
30547
- if (this.waitWhile(STATE_CONTINUE) !== STATE_RESPONSE) throw new SyncChannelClosedError();
30548
- }
30549
- Atomics.store(this.control, STATE, STATE_IDLE);
30550
- if (parts.length === 1) return parts[0];
30551
- const joined = new Uint8Array(total);
30552
- let at2 = 0;
30553
- for (const part of parts) {
30554
- joined.set(part, at2);
30555
- at2 += part.length;
30556
- }
30557
- return joined;
30558
- }
30559
- publish(state) {
30560
- Atomics.store(this.control, STATE, state);
30561
- Atomics.notify(this.control, STATE);
30562
- this.wake();
30563
- }
30564
- /**
30565
- * Block until the state moves off `state`.
30566
- *
30567
- * Waking periodically rather than waiting forever is what lets a torn-down
30568
- * host release a worker instead of stranding it.
30569
- */
30570
- waitWhile(state) {
30571
- for (; ; ) {
30572
- Atomics.wait(this.control, STATE, state, WAIT_SLICE_MS);
30573
- const current2 = Atomics.load(this.control, STATE);
30574
- if (current2 !== state) return current2;
30575
- }
30576
- }
30577
- };
30578
-
30579
30607
  // src/runtime/sync-syscalls.ts
30580
30608
  var SPAWN_OP = "spawnSync";
30581
30609
  function spawnSyncOverChannel(channel, request) {
@@ -30652,6 +30680,7 @@ async function run(port, start) {
30652
30680
  const router = new VirtualHttpRouter();
30653
30681
  const owner = "worker";
30654
30682
  router.onListen = (listening) => port.postMessage({ type: "listen", port: listening });
30683
+ router.onClose = (closed) => port.postMessage({ type: "close-port", port: closed });
30655
30684
  let requestedExit = 0;
30656
30685
  let engine;
30657
30686
  let finished = false;
@@ -30680,6 +30709,8 @@ async function run(port, start) {
30680
30709
  const finish = (code) => {
30681
30710
  if (finished) return;
30682
30711
  finished = true;
30712
+ core.cancelTimers();
30713
+ router.closeOwner(owner);
30683
30714
  port.postMessage({ type: "exit", code });
30684
30715
  };
30685
30716
  port.onMessage((message) => {
@@ -30701,7 +30732,6 @@ async function run(port, start) {
30701
30732
  try {
30702
30733
  await engine.run(start.script);
30703
30734
  await settle(router, owner, core, () => finished);
30704
- if (router.activePorts(owner).length) return;
30705
30735
  finish(requestedExit);
30706
30736
  } catch (error) {
30707
30737
  if (error instanceof ProcessExit) {
@@ -30713,18 +30743,12 @@ async function run(port, start) {
30713
30743
  }
30714
30744
  }
30715
30745
  async function settle(router, owner, core, killed) {
30716
- for (let turn = 0; turn < DRAIN_TURNS; turn++) {
30717
- if (router.activePorts(owner).length || killed()) return;
30718
- await new Promise((resolve2) => setTimeout(resolve2, 0));
30719
- }
30720
- if (core.pendingHandles() === 0 && core.pendingRequests() === 0 && core.pendingUnrefed() > 0 && !core.readingStdin()) {
30721
- const deadline = Date.now() + 1e3;
30722
- while (!router.activePorts(owner).length && !killed() && Date.now() < deadline) {
30723
- await new Promise((resolve2) => setTimeout(resolve2, 5));
30724
- }
30725
- }
30726
- while (!router.activePorts(owner).length && !killed() && (core.pendingHandles() > 0 || core.pendingRequests() > 0 || core.readingStdin())) {
30727
- await new Promise((resolve2) => setTimeout(resolve2, 5));
30746
+ let idleTurns = 0;
30747
+ while (!killed()) {
30748
+ const busy = router.referencedPorts(owner).length > 0 || core.pendingHandles() > 0 || core.pendingRequests() > 0 || core.readingStdin();
30749
+ idleTurns = busy ? 0 : idleTurns + 1;
30750
+ if (idleTurns >= DRAIN_TURNS) return;
30751
+ await new Promise((resolve2) => setTimeout(resolve2, busy ? 5 : 0));
30728
30752
  }
30729
30753
  }
30730
30754
  async function answer(port, router, message) {