sandboxedjs 0.1.44 → 0.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -23472,8 +23472,17 @@ var VirtualIncomingMessage = class extends streamModule4.Readable {
23472
23472
  httpVersionMajor = 1;
23473
23473
  httpVersionMinor = 1;
23474
23474
  complete = true;
23475
+ /*
23476
+ * Mutable, and `connection` a getter over it, because an upgraded request
23477
+ * carries a real socket rather than the stub. `emit("upgrade", req, socket)`
23478
+ * hands the same object to both places, and a library that reaches it as
23479
+ * `req.socket` — rather than through the argument — has to find the one it
23480
+ * can actually write to.
23481
+ */
23475
23482
  socket = socketStub();
23476
- connection = this.socket;
23483
+ get connection() {
23484
+ return this.socket;
23485
+ }
23477
23486
  constructor(init) {
23478
23487
  super();
23479
23488
  this.method = (init.method ?? "GET").toUpperCase();
@@ -23579,6 +23588,80 @@ var VirtualServerResponse = class extends streamModule4.Writable {
23579
23588
  return this;
23580
23589
  }
23581
23590
  };
23591
+ var VirtualSocket = class extends streamModule4.Duplex {
23592
+ remoteAddress = "127.0.0.1";
23593
+ remotePort = 0;
23594
+ localAddress = "127.0.0.1";
23595
+ localPort = 0;
23596
+ encrypted = false;
23597
+ bufferSize = 0;
23598
+ peer;
23599
+ /** Set once the peer is told, so `end()` then `destroy()` reports once. */
23600
+ hungUp = false;
23601
+ constructor(peer) {
23602
+ super({ allowHalfOpen: false });
23603
+ this.peer = peer;
23604
+ }
23605
+ _read() {
23606
+ }
23607
+ _write(chunk, encoding, callback) {
23608
+ const bytes2 = Buffer2.isBuffer(chunk) ? chunk : Buffer2.from(chunk, encoding);
23609
+ try {
23610
+ this.peer?.data(new Uint8Array(bytes2));
23611
+ callback();
23612
+ } catch (error) {
23613
+ callback(error);
23614
+ }
23615
+ }
23616
+ _final(callback) {
23617
+ this.hangUp();
23618
+ callback();
23619
+ }
23620
+ _destroy(error, callback) {
23621
+ this.hangUp();
23622
+ callback(error);
23623
+ }
23624
+ hangUp() {
23625
+ if (this.hungUp) return;
23626
+ this.hungUp = true;
23627
+ try {
23628
+ this.peer?.close();
23629
+ } catch {
23630
+ }
23631
+ this.peer = null;
23632
+ }
23633
+ /** Bytes arriving from the far end. */
23634
+ deliver(bytes2) {
23635
+ if (this.destroyed || this.hungUp) return;
23636
+ this.push(Buffer2.from(bytes2));
23637
+ }
23638
+ /** The far end went away; let the server's stream end cleanly. */
23639
+ peerClosed() {
23640
+ if (this.destroyed) return;
23641
+ this.push(null);
23642
+ }
23643
+ /* The parts of `net.Socket` a WebSocket library reaches for. None of them
23644
+ * mean anything without a kernel, but every one of them is called. */
23645
+ setNoDelay() {
23646
+ return this;
23647
+ }
23648
+ setKeepAlive() {
23649
+ return this;
23650
+ }
23651
+ setTimeout(_milliseconds, callback) {
23652
+ if (callback) this.once("timeout", callback);
23653
+ return this;
23654
+ }
23655
+ destroySoon() {
23656
+ this.end();
23657
+ }
23658
+ ref() {
23659
+ return this;
23660
+ }
23661
+ unref() {
23662
+ return this;
23663
+ }
23664
+ };
23582
23665
  var VirtualHttpServer = class extends EventEmitter4 {
23583
23666
  constructor(router, owner, listener) {
23584
23667
  super();
@@ -23678,6 +23761,33 @@ var VirtualHttpRouter = class {
23678
23761
  return { statusCode: 500, statusMessage: "Internal Server Error", headers: {}, body: error instanceof Error ? error.message : String(error) };
23679
23762
  }
23680
23763
  }
23764
+ /**
23765
+ * Open a connection that leaves HTTP behind — a WebSocket, in practice.
23766
+ *
23767
+ * The counterpart to {@link request}, and the thing whose absence made a dev
23768
+ * server look broken. Every Node WebSocket library is built the same way: it
23769
+ * hands `http.Server` an `upgrade` listener and waits. Nothing here ever
23770
+ * emitted one, so `ws` sat holding a server that could not receive a single
23771
+ * connection, and Vite lost the channel it uses to tell a page to reload —
23772
+ * which is the only way it can recover after re-optimizing dependencies.
23773
+ *
23774
+ * Null means there is nothing to connect to: no server on the port, or a
23775
+ * server that never asked for upgrades. Both are refusals the caller should
23776
+ * report rather than wait out.
23777
+ */
23778
+ connect(port, init, peer) {
23779
+ const item = this.servers.get(port);
23780
+ if (!item) return null;
23781
+ if (item.server.listenerCount("upgrade") === 0) return null;
23782
+ const request = new VirtualIncomingMessage(init);
23783
+ const socket = new VirtualSocket(peer);
23784
+ request.socket = socket;
23785
+ item.server.emit("upgrade", request, socket, Buffer2.alloc(0));
23786
+ return {
23787
+ send: (bytes2) => socket.deliver(bytes2),
23788
+ close: () => socket.peerClosed()
23789
+ };
23790
+ }
23681
23791
  };
23682
23792
  var VirtualClientResponse = class extends streamModule4.Readable {
23683
23793
  constructor(statusCode, statusMessage, headers, body) {
@@ -27194,6 +27304,9 @@ var LocalRuntimePod = class _LocalRuntimePod {
27194
27304
  async request(_port, _init = {}) {
27195
27305
  return this.router.request(_port, _init);
27196
27306
  }
27307
+ connect(port, init, peer) {
27308
+ return this.router.connect(port, init, peer);
27309
+ }
27197
27310
  snapshot() {
27198
27311
  this.assertActive();
27199
27312
  return this.volume.snapshot();
@@ -27629,6 +27742,15 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27629
27742
  case "http-response":
27630
27743
  this.settleProxied(Number(message.id), message.response);
27631
27744
  return;
27745
+ case "ws-data":
27746
+ this.upgraded.get(Number(message.id))?.write(new Uint8Array(message.data));
27747
+ return;
27748
+ case "ws-close": {
27749
+ const socket = this.upgraded.get(Number(message.id));
27750
+ this.upgraded.delete(Number(message.id));
27751
+ socket?.end();
27752
+ return;
27753
+ }
27632
27754
  case "child-start":
27633
27755
  this.startChild(worker, children, message, ownedChildren);
27634
27756
  return;
@@ -27717,6 +27839,8 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27717
27839
  // ── HTTP servers living on another thread ─────────────────────────────────
27718
27840
  proxies = /* @__PURE__ */ new Map();
27719
27841
  waiting = /* @__PURE__ */ new Map();
27842
+ /** Upgraded connections, by the id the Worker knows them as. */
27843
+ upgraded = /* @__PURE__ */ new Map();
27720
27844
  nextRequestId = 1;
27721
27845
  /**
27722
27846
  * Register a stand-in for a server that is actually running in the Worker.
@@ -27729,6 +27853,9 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27729
27853
  const server = new VirtualHttpServer(this.router, owner, (request, response) => {
27730
27854
  void this.forward(worker, port, request, response);
27731
27855
  });
27856
+ server.on("upgrade", (request, socket) => {
27857
+ this.upgrade(worker, port, request, socket);
27858
+ });
27732
27859
  try {
27733
27860
  server.listen(port);
27734
27861
  this.proxies.set(port, server);
@@ -27759,6 +27886,33 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27759
27886
  response.writeHead(Number(result.statusCode ?? 200), String(result.statusMessage ?? ""), result.headers ?? {});
27760
27887
  response.end(result.body ?? new Uint8Array());
27761
27888
  }
27889
+ /**
27890
+ * Tunnel one upgraded connection to the server that actually holds the port.
27891
+ *
27892
+ * Unlike {@link forward} there is no reply to wait for: both ends write
27893
+ * whenever they have something, until one of them stops. The id is what ties
27894
+ * the two directions together across the Worker boundary.
27895
+ */
27896
+ upgrade(worker, port, request, socket) {
27897
+ const id = this.nextRequestId++;
27898
+ this.upgraded.set(id, socket);
27899
+ socket.on("data", (chunk) => {
27900
+ const bytes2 = new Uint8Array(chunk);
27901
+ worker.postMessage({ type: "ws-data", id, data: bytes2 });
27902
+ });
27903
+ const drop = () => {
27904
+ if (!this.upgraded.delete(id)) return;
27905
+ worker.postMessage({ type: "ws-close", id });
27906
+ };
27907
+ socket.on("close", drop);
27908
+ socket.on("error", drop);
27909
+ worker.postMessage({
27910
+ type: "ws-open",
27911
+ id,
27912
+ port,
27913
+ init: { method: request.method, path: request.url, headers: request.headers }
27914
+ });
27915
+ }
27762
27916
  settleProxied(id, response) {
27763
27917
  const resolve2 = this.waiting.get(id);
27764
27918
  if (!resolve2) return;
@@ -27771,6 +27925,10 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27771
27925
  server.close();
27772
27926
  this.proxies.delete(port);
27773
27927
  }
27928
+ for (const [id, socket] of [...this.upgraded]) {
27929
+ this.upgraded.delete(id);
27930
+ socket.destroy();
27931
+ }
27774
27932
  }
27775
27933
  teardown() {
27776
27934
  for (const { worker, server } of [...this.live]) {
@@ -28144,6 +28302,29 @@ var Container = class _Container {
28144
28302
  }
28145
28303
  };
28146
28304
  }
28305
+ /**
28306
+ * Open a connection to a server inside the container that upgrades out of
28307
+ * HTTP — in practice, a WebSocket.
28308
+ *
28309
+ * The counterpart to {@link request}, and the thing a dev server needs that
28310
+ * a request cannot provide. Bytes go in with `send`, come back through
28311
+ * `peer.data`, and neither side interprets them: the container runs whatever
28312
+ * WebSocket library the program chose, and the caller is responsible for
28313
+ * speaking the protocol it answers with.
28314
+ *
28315
+ * Null means there is nothing to talk to — no server on the port, or one
28316
+ * that never registered an `upgrade` handler. Both are worth reporting
28317
+ * rather than waiting out, because neither resolves on its own.
28318
+ */
28319
+ connect(port, init = {}, peer) {
28320
+ this.assertActive();
28321
+ if (!this.pod.connect) return null;
28322
+ return this.pod.connect(
28323
+ port,
28324
+ { method: init.method ?? "GET", path: init.path ?? "/", headers: init.headers ?? {} },
28325
+ peer
28326
+ );
28327
+ }
28147
28328
  /**
28148
28329
  * Deliver a request whose body is bytes, without letting them become text.
28149
28330
  *
@@ -28715,6 +28896,12 @@ init_arith();
28715
28896
  init_expand();
28716
28897
  init_builtins();
28717
28898
 
28899
+ // src/preview/ws-client.ts
28900
+ var WS_OPEN = "sandboxedjs:ws-open";
28901
+ var WS_DATA = "sandboxedjs:ws-data";
28902
+ var WS_CLOSE = "sandboxedjs:ws-close";
28903
+ var WS_ERROR = "sandboxedjs:ws-error";
28904
+
28718
28905
  // src/preview/register.ts
28719
28906
  function serveContainerOn(port, box) {
28720
28907
  port.onmessage = async (event) => {
@@ -28800,13 +28987,65 @@ async function createPreview(box, options = {}) {
28800
28987
  channel = new MessageChannel();
28801
28988
  serveContainerOn(channel.port1, box);
28802
28989
  const target = registration.active ?? worker;
28803
- target.postMessage({ type: "sandboxedjs:connect" }, [channel.port2]);
28990
+ target.postMessage({ type: "sandboxedjs:connect", injectSockets: options.websocket !== false }, [channel.port2]);
28804
28991
  };
28805
28992
  const onWorkerMessage = (event) => {
28806
- if (event.data?.type === "sandboxedjs:host-needed") connect();
28993
+ const type = event.data?.type;
28994
+ if (type === "sandboxedjs:host-needed") connect();
28995
+ if (type === "sandboxedjs:stale") options.onStale?.();
28807
28996
  };
28808
28997
  navigator.serviceWorker.addEventListener("message", onWorkerMessage);
28809
28998
  connect();
28999
+ const sockets = /* @__PURE__ */ new Map();
29000
+ const onFrameMessage = (event) => {
29001
+ if (event.origin !== location.origin || !event.source) return;
29002
+ const data = event.data;
29003
+ if (!data || typeof data !== "object") return;
29004
+ if (data.type !== WS_OPEN && data.type !== WS_DATA && data.type !== WS_CLOSE) return;
29005
+ const source = event.source;
29006
+ const id = Number(data.id);
29007
+ const reply = (message, transfer = []) => source.postMessage(message, location.origin, transfer);
29008
+ if (data.type === WS_OPEN) {
29009
+ let table2 = sockets.get(source);
29010
+ if (!table2) sockets.set(source, table2 = /* @__PURE__ */ new Map());
29011
+ let opened = null;
29012
+ try {
29013
+ opened = box.connect(
29014
+ Number(data.port),
29015
+ { method: "GET", path: data.path ?? "/", headers: data.headers ?? {} },
29016
+ {
29017
+ data: (bytes2) => {
29018
+ const carrier = bytes2.slice().buffer;
29019
+ reply({ type: WS_DATA, id, data: carrier }, [carrier]);
29020
+ },
29021
+ close: () => {
29022
+ table2.delete(id);
29023
+ reply({ type: WS_CLOSE, id });
29024
+ }
29025
+ }
29026
+ );
29027
+ } catch (error) {
29028
+ opened = null;
29029
+ reply({ type: WS_ERROR, id, message: error instanceof Error ? error.message : String(error) });
29030
+ return;
29031
+ }
29032
+ if (!opened) {
29033
+ reply({ type: WS_ERROR, id, message: `Nothing is accepting WebSocket connections on port ${data.port}.` });
29034
+ return;
29035
+ }
29036
+ table2.set(id, opened);
29037
+ return;
29038
+ }
29039
+ const table = sockets.get(source);
29040
+ const socket = table?.get(id);
29041
+ if (!socket) return;
29042
+ if (data.type === WS_DATA) socket.send(new Uint8Array(data.data));
29043
+ else {
29044
+ socket.close();
29045
+ table.delete(id);
29046
+ }
29047
+ };
29048
+ if (typeof window !== "undefined") window.addEventListener("message", onFrameMessage);
28810
29049
  const base2 = registration.scope.replace(/\/$/, "");
28811
29050
  const urlFor = (port) => `${base2}/__sbx__/${port}/`;
28812
29051
  return {
@@ -28820,6 +29059,9 @@ async function createPreview(box, options = {}) {
28820
29059
  },
28821
29060
  dispose: async () => {
28822
29061
  navigator.serviceWorker.removeEventListener("message", onWorkerMessage);
29062
+ if (typeof window !== "undefined") window.removeEventListener("message", onFrameMessage);
29063
+ for (const table of sockets.values()) for (const socket of table.values()) socket.close();
29064
+ sockets.clear();
28823
29065
  channel?.port1.close();
28824
29066
  channel = null;
28825
29067
  await registration.unregister();