sandboxedjs 0.1.45 → 0.1.47

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
@@ -20301,6 +20301,8 @@ var CleanPackageInstaller = class _CleanPackageInstaller {
20301
20301
  fetcher;
20302
20302
  metadata = /* @__PURE__ */ new Map();
20303
20303
  tarballs = /* @__PURE__ */ new Map();
20304
+ /** Single-version manifests, for the fields the packument omits. */
20305
+ details = /* @__PURE__ */ new Map();
20304
20306
  forCwd(cwd) {
20305
20307
  return new _CleanPackageInstaller(this.volume, { ...this.options, cwd });
20306
20308
  }
@@ -20326,10 +20328,17 @@ var CleanPackageInstaller = class _CleanPackageInstaller {
20326
20328
  const version = resolveVersion(metadata, range);
20327
20329
  const manifest = metadata.versions[version];
20328
20330
  if (!manifest) throw new Error(`No matching version found for ${name}@${range}`);
20329
- if (!supportsPlatform(manifest)) {
20330
- throw new Error(`${name}@${version} is not compatible with linux/x64/glibc`);
20331
- }
20332
20331
  const identity = `${name}@${version}`;
20332
+ if (!supportsPlatform(manifest)) throw new IncompatiblePlatform(identity, "");
20333
+ if (manifest.os?.length || manifest.cpu?.length) {
20334
+ const detail = await this.platformDetail(name, version);
20335
+ if (!platformListAllows(detail.libc, PLATFORM.libc)) {
20336
+ throw new IncompatiblePlatform(identity, ` (needs ${detail.libc?.join(", ")})`);
20337
+ }
20338
+ if (detail.main?.endsWith(".node")) {
20339
+ throw new IncompatiblePlatform(identity, " (native addon; this runtime cannot dlopen)");
20340
+ }
20341
+ }
20333
20342
  const target = join(modulesRoot, name);
20334
20343
  const installed2 = this.tryReadJson(join(target, "package.json"));
20335
20344
  if (installed2?.version === version) return installed2;
@@ -20351,11 +20360,44 @@ var CleanPackageInstaller = class _CleanPackageInstaller {
20351
20360
  try {
20352
20361
  await this.installAt(dependency, dependencyRange, childRoot, options, nextAncestry);
20353
20362
  } catch (error) {
20363
+ if (error instanceof IncompatiblePlatform) continue;
20354
20364
  options.onProgress?.(`Skipped optional ${dependency}: ${error instanceof Error ? error.message : String(error)}`);
20355
20365
  }
20356
20366
  }
20357
20367
  return manifest;
20358
20368
  }
20369
+ /**
20370
+ * The `libc` and `main` fields, which the abbreviated packument leaves out.
20371
+ *
20372
+ * Fetched per version rather than as a full packument: the single-version
20373
+ * document is a few kilobytes, while the full one for a popular package runs
20374
+ * to megabytes. Only packages that already declare `os` or `cpu` ask for it,
20375
+ * so an ordinary install of pure-JavaScript dependencies makes no extra
20376
+ * requests at all — it is the prebuilt binaries, a handful per project, that
20377
+ * need the answer.
20378
+ *
20379
+ * A failure here is not fatal. Being unable to read `libc` puts the check
20380
+ * back where it was before this existed, which is worth strictly less than
20381
+ * being right and strictly more than refusing to install.
20382
+ */
20383
+ async platformDetail(name, version) {
20384
+ const key = `${name}@${version}`;
20385
+ let request = this.details.get(key);
20386
+ if (!request) {
20387
+ request = (async () => {
20388
+ try {
20389
+ const encoded = name.startsWith("@") ? name.replace("/", "%2F") : encodeURIComponent(name);
20390
+ const response = await this.fetcher(`${this.registry}/${encoded}/${version}`);
20391
+ if (!response.ok) return {};
20392
+ return await response.json();
20393
+ } catch {
20394
+ return {};
20395
+ }
20396
+ })();
20397
+ this.details.set(key, request);
20398
+ }
20399
+ return request;
20400
+ }
20359
20401
  async getMetadata(name) {
20360
20402
  let request = this.metadata.get(name);
20361
20403
  if (!request) {
@@ -20423,8 +20465,15 @@ var CleanPackageInstaller = class _CleanPackageInstaller {
20423
20465
  }
20424
20466
  }
20425
20467
  };
20468
+ var PLATFORM = { os: "linux", cpu: "x64", libc: "glibc" };
20469
+ var IncompatiblePlatform = class extends Error {
20470
+ constructor(identity, detail) {
20471
+ super(`${identity} is not compatible with ${PLATFORM.os}/${PLATFORM.cpu}/${PLATFORM.libc}${detail}`);
20472
+ this.name = "IncompatiblePlatform";
20473
+ }
20474
+ };
20426
20475
  function supportsPlatform(manifest) {
20427
- return !manifest.main?.endsWith(".node") && platformListAllows(manifest.os, "linux") && platformListAllows(manifest.cpu, "x64") && platformListAllows(manifest.libc, "glibc");
20476
+ return platformListAllows(manifest.os, PLATFORM.os) && platformListAllows(manifest.cpu, PLATFORM.cpu) && platformListAllows(manifest.libc, PLATFORM.libc);
20428
20477
  }
20429
20478
  function platformListAllows(values, current) {
20430
20479
  if (!values?.length) return true;
@@ -23472,8 +23521,17 @@ var VirtualIncomingMessage = class extends streamModule4.Readable {
23472
23521
  httpVersionMajor = 1;
23473
23522
  httpVersionMinor = 1;
23474
23523
  complete = true;
23524
+ /*
23525
+ * Mutable, and `connection` a getter over it, because an upgraded request
23526
+ * carries a real socket rather than the stub. `emit("upgrade", req, socket)`
23527
+ * hands the same object to both places, and a library that reaches it as
23528
+ * `req.socket` — rather than through the argument — has to find the one it
23529
+ * can actually write to.
23530
+ */
23475
23531
  socket = socketStub();
23476
- connection = this.socket;
23532
+ get connection() {
23533
+ return this.socket;
23534
+ }
23477
23535
  constructor(init) {
23478
23536
  super();
23479
23537
  this.method = (init.method ?? "GET").toUpperCase();
@@ -23579,6 +23637,80 @@ var VirtualServerResponse = class extends streamModule4.Writable {
23579
23637
  return this;
23580
23638
  }
23581
23639
  };
23640
+ var VirtualSocket = class extends streamModule4.Duplex {
23641
+ remoteAddress = "127.0.0.1";
23642
+ remotePort = 0;
23643
+ localAddress = "127.0.0.1";
23644
+ localPort = 0;
23645
+ encrypted = false;
23646
+ bufferSize = 0;
23647
+ peer;
23648
+ /** Set once the peer is told, so `end()` then `destroy()` reports once. */
23649
+ hungUp = false;
23650
+ constructor(peer) {
23651
+ super({ allowHalfOpen: false });
23652
+ this.peer = peer;
23653
+ }
23654
+ _read() {
23655
+ }
23656
+ _write(chunk, encoding, callback) {
23657
+ const bytes2 = Buffer2.isBuffer(chunk) ? chunk : Buffer2.from(chunk, encoding);
23658
+ try {
23659
+ this.peer?.data(new Uint8Array(bytes2));
23660
+ callback();
23661
+ } catch (error) {
23662
+ callback(error);
23663
+ }
23664
+ }
23665
+ _final(callback) {
23666
+ this.hangUp();
23667
+ callback();
23668
+ }
23669
+ _destroy(error, callback) {
23670
+ this.hangUp();
23671
+ callback(error);
23672
+ }
23673
+ hangUp() {
23674
+ if (this.hungUp) return;
23675
+ this.hungUp = true;
23676
+ try {
23677
+ this.peer?.close();
23678
+ } catch {
23679
+ }
23680
+ this.peer = null;
23681
+ }
23682
+ /** Bytes arriving from the far end. */
23683
+ deliver(bytes2) {
23684
+ if (this.destroyed || this.hungUp) return;
23685
+ this.push(Buffer2.from(bytes2));
23686
+ }
23687
+ /** The far end went away; let the server's stream end cleanly. */
23688
+ peerClosed() {
23689
+ if (this.destroyed) return;
23690
+ this.push(null);
23691
+ }
23692
+ /* The parts of `net.Socket` a WebSocket library reaches for. None of them
23693
+ * mean anything without a kernel, but every one of them is called. */
23694
+ setNoDelay() {
23695
+ return this;
23696
+ }
23697
+ setKeepAlive() {
23698
+ return this;
23699
+ }
23700
+ setTimeout(_milliseconds, callback) {
23701
+ if (callback) this.once("timeout", callback);
23702
+ return this;
23703
+ }
23704
+ destroySoon() {
23705
+ this.end();
23706
+ }
23707
+ ref() {
23708
+ return this;
23709
+ }
23710
+ unref() {
23711
+ return this;
23712
+ }
23713
+ };
23582
23714
  var VirtualHttpServer = class extends EventEmitter4 {
23583
23715
  constructor(router, owner, listener) {
23584
23716
  super();
@@ -23678,6 +23810,33 @@ var VirtualHttpRouter = class {
23678
23810
  return { statusCode: 500, statusMessage: "Internal Server Error", headers: {}, body: error instanceof Error ? error.message : String(error) };
23679
23811
  }
23680
23812
  }
23813
+ /**
23814
+ * Open a connection that leaves HTTP behind — a WebSocket, in practice.
23815
+ *
23816
+ * The counterpart to {@link request}, and the thing whose absence made a dev
23817
+ * server look broken. Every Node WebSocket library is built the same way: it
23818
+ * hands `http.Server` an `upgrade` listener and waits. Nothing here ever
23819
+ * emitted one, so `ws` sat holding a server that could not receive a single
23820
+ * connection, and Vite lost the channel it uses to tell a page to reload —
23821
+ * which is the only way it can recover after re-optimizing dependencies.
23822
+ *
23823
+ * Null means there is nothing to connect to: no server on the port, or a
23824
+ * server that never asked for upgrades. Both are refusals the caller should
23825
+ * report rather than wait out.
23826
+ */
23827
+ connect(port, init, peer) {
23828
+ const item = this.servers.get(port);
23829
+ if (!item) return null;
23830
+ if (item.server.listenerCount("upgrade") === 0) return null;
23831
+ const request = new VirtualIncomingMessage(init);
23832
+ const socket = new VirtualSocket(peer);
23833
+ request.socket = socket;
23834
+ item.server.emit("upgrade", request, socket, Buffer2.alloc(0));
23835
+ return {
23836
+ send: (bytes2) => socket.deliver(bytes2),
23837
+ close: () => socket.peerClosed()
23838
+ };
23839
+ }
23681
23840
  };
23682
23841
  var VirtualClientResponse = class extends streamModule4.Readable {
23683
23842
  constructor(statusCode, statusMessage, headers, body) {
@@ -27194,6 +27353,9 @@ var LocalRuntimePod = class _LocalRuntimePod {
27194
27353
  async request(_port, _init = {}) {
27195
27354
  return this.router.request(_port, _init);
27196
27355
  }
27356
+ connect(port, init, peer) {
27357
+ return this.router.connect(port, init, peer);
27358
+ }
27197
27359
  snapshot() {
27198
27360
  this.assertActive();
27199
27361
  return this.volume.snapshot();
@@ -27629,6 +27791,15 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27629
27791
  case "http-response":
27630
27792
  this.settleProxied(Number(message.id), message.response);
27631
27793
  return;
27794
+ case "ws-data":
27795
+ this.upgraded.get(Number(message.id))?.write(new Uint8Array(message.data));
27796
+ return;
27797
+ case "ws-close": {
27798
+ const socket = this.upgraded.get(Number(message.id));
27799
+ this.upgraded.delete(Number(message.id));
27800
+ socket?.end();
27801
+ return;
27802
+ }
27632
27803
  case "child-start":
27633
27804
  this.startChild(worker, children, message, ownedChildren);
27634
27805
  return;
@@ -27717,6 +27888,8 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27717
27888
  // ── HTTP servers living on another thread ─────────────────────────────────
27718
27889
  proxies = /* @__PURE__ */ new Map();
27719
27890
  waiting = /* @__PURE__ */ new Map();
27891
+ /** Upgraded connections, by the id the Worker knows them as. */
27892
+ upgraded = /* @__PURE__ */ new Map();
27720
27893
  nextRequestId = 1;
27721
27894
  /**
27722
27895
  * Register a stand-in for a server that is actually running in the Worker.
@@ -27729,6 +27902,9 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27729
27902
  const server = new VirtualHttpServer(this.router, owner, (request, response) => {
27730
27903
  void this.forward(worker, port, request, response);
27731
27904
  });
27905
+ server.on("upgrade", (request, socket) => {
27906
+ this.upgrade(worker, port, request, socket);
27907
+ });
27732
27908
  try {
27733
27909
  server.listen(port);
27734
27910
  this.proxies.set(port, server);
@@ -27759,6 +27935,33 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27759
27935
  response.writeHead(Number(result.statusCode ?? 200), String(result.statusMessage ?? ""), result.headers ?? {});
27760
27936
  response.end(result.body ?? new Uint8Array());
27761
27937
  }
27938
+ /**
27939
+ * Tunnel one upgraded connection to the server that actually holds the port.
27940
+ *
27941
+ * Unlike {@link forward} there is no reply to wait for: both ends write
27942
+ * whenever they have something, until one of them stops. The id is what ties
27943
+ * the two directions together across the Worker boundary.
27944
+ */
27945
+ upgrade(worker, port, request, socket) {
27946
+ const id = this.nextRequestId++;
27947
+ this.upgraded.set(id, socket);
27948
+ socket.on("data", (chunk) => {
27949
+ const bytes2 = new Uint8Array(chunk);
27950
+ worker.postMessage({ type: "ws-data", id, data: bytes2 });
27951
+ });
27952
+ const drop = () => {
27953
+ if (!this.upgraded.delete(id)) return;
27954
+ worker.postMessage({ type: "ws-close", id });
27955
+ };
27956
+ socket.on("close", drop);
27957
+ socket.on("error", drop);
27958
+ worker.postMessage({
27959
+ type: "ws-open",
27960
+ id,
27961
+ port,
27962
+ init: { method: request.method, path: request.url, headers: request.headers }
27963
+ });
27964
+ }
27762
27965
  settleProxied(id, response) {
27763
27966
  const resolve2 = this.waiting.get(id);
27764
27967
  if (!resolve2) return;
@@ -27771,6 +27974,10 @@ var WorkerRuntimePod = class _WorkerRuntimePod extends LocalRuntimePod {
27771
27974
  server.close();
27772
27975
  this.proxies.delete(port);
27773
27976
  }
27977
+ for (const [id, socket] of [...this.upgraded]) {
27978
+ this.upgraded.delete(id);
27979
+ socket.destroy();
27980
+ }
27774
27981
  }
27775
27982
  teardown() {
27776
27983
  for (const { worker, server } of [...this.live]) {
@@ -28144,6 +28351,29 @@ var Container = class _Container {
28144
28351
  }
28145
28352
  };
28146
28353
  }
28354
+ /**
28355
+ * Open a connection to a server inside the container that upgrades out of
28356
+ * HTTP — in practice, a WebSocket.
28357
+ *
28358
+ * The counterpart to {@link request}, and the thing a dev server needs that
28359
+ * a request cannot provide. Bytes go in with `send`, come back through
28360
+ * `peer.data`, and neither side interprets them: the container runs whatever
28361
+ * WebSocket library the program chose, and the caller is responsible for
28362
+ * speaking the protocol it answers with.
28363
+ *
28364
+ * Null means there is nothing to talk to — no server on the port, or one
28365
+ * that never registered an `upgrade` handler. Both are worth reporting
28366
+ * rather than waiting out, because neither resolves on its own.
28367
+ */
28368
+ connect(port, init = {}, peer) {
28369
+ this.assertActive();
28370
+ if (!this.pod.connect) return null;
28371
+ return this.pod.connect(
28372
+ port,
28373
+ { method: init.method ?? "GET", path: init.path ?? "/", headers: init.headers ?? {} },
28374
+ peer
28375
+ );
28376
+ }
28147
28377
  /**
28148
28378
  * Deliver a request whose body is bytes, without letting them become text.
28149
28379
  *
@@ -28715,6 +28945,12 @@ init_arith();
28715
28945
  init_expand();
28716
28946
  init_builtins();
28717
28947
 
28948
+ // src/preview/ws-client.ts
28949
+ var WS_OPEN = "sandboxedjs:ws-open";
28950
+ var WS_DATA = "sandboxedjs:ws-data";
28951
+ var WS_CLOSE = "sandboxedjs:ws-close";
28952
+ var WS_ERROR = "sandboxedjs:ws-error";
28953
+
28718
28954
  // src/preview/register.ts
28719
28955
  function serveContainerOn(port, box) {
28720
28956
  port.onmessage = async (event) => {
@@ -28800,7 +29036,7 @@ async function createPreview(box, options = {}) {
28800
29036
  channel = new MessageChannel();
28801
29037
  serveContainerOn(channel.port1, box);
28802
29038
  const target = registration.active ?? worker;
28803
- target.postMessage({ type: "sandboxedjs:connect" }, [channel.port2]);
29039
+ target.postMessage({ type: "sandboxedjs:connect", injectSockets: options.websocket !== false }, [channel.port2]);
28804
29040
  };
28805
29041
  const onWorkerMessage = (event) => {
28806
29042
  const type = event.data?.type;
@@ -28809,6 +29045,56 @@ async function createPreview(box, options = {}) {
28809
29045
  };
28810
29046
  navigator.serviceWorker.addEventListener("message", onWorkerMessage);
28811
29047
  connect();
29048
+ const sockets = /* @__PURE__ */ new Map();
29049
+ const onFrameMessage = (event) => {
29050
+ if (event.origin !== location.origin || !event.source) return;
29051
+ const data = event.data;
29052
+ if (!data || typeof data !== "object") return;
29053
+ if (data.type !== WS_OPEN && data.type !== WS_DATA && data.type !== WS_CLOSE) return;
29054
+ const source = event.source;
29055
+ const id = Number(data.id);
29056
+ const reply = (message, transfer = []) => source.postMessage(message, location.origin, transfer);
29057
+ if (data.type === WS_OPEN) {
29058
+ let table2 = sockets.get(source);
29059
+ if (!table2) sockets.set(source, table2 = /* @__PURE__ */ new Map());
29060
+ let opened = null;
29061
+ try {
29062
+ opened = box.connect(
29063
+ Number(data.port),
29064
+ { method: "GET", path: data.path ?? "/", headers: data.headers ?? {} },
29065
+ {
29066
+ data: (bytes2) => {
29067
+ const carrier = bytes2.slice().buffer;
29068
+ reply({ type: WS_DATA, id, data: carrier }, [carrier]);
29069
+ },
29070
+ close: () => {
29071
+ table2.delete(id);
29072
+ reply({ type: WS_CLOSE, id });
29073
+ }
29074
+ }
29075
+ );
29076
+ } catch (error) {
29077
+ opened = null;
29078
+ reply({ type: WS_ERROR, id, message: error instanceof Error ? error.message : String(error) });
29079
+ return;
29080
+ }
29081
+ if (!opened) {
29082
+ reply({ type: WS_ERROR, id, message: `Nothing is accepting WebSocket connections on port ${data.port}.` });
29083
+ return;
29084
+ }
29085
+ table2.set(id, opened);
29086
+ return;
29087
+ }
29088
+ const table = sockets.get(source);
29089
+ const socket = table?.get(id);
29090
+ if (!socket) return;
29091
+ if (data.type === WS_DATA) socket.send(new Uint8Array(data.data));
29092
+ else {
29093
+ socket.close();
29094
+ table.delete(id);
29095
+ }
29096
+ };
29097
+ if (typeof window !== "undefined") window.addEventListener("message", onFrameMessage);
28812
29098
  const base2 = registration.scope.replace(/\/$/, "");
28813
29099
  const urlFor = (port) => `${base2}/__sbx__/${port}/`;
28814
29100
  return {
@@ -28822,6 +29108,9 @@ async function createPreview(box, options = {}) {
28822
29108
  },
28823
29109
  dispose: async () => {
28824
29110
  navigator.serviceWorker.removeEventListener("message", onWorkerMessage);
29111
+ if (typeof window !== "undefined") window.removeEventListener("message", onFrameMessage);
29112
+ for (const table of sockets.values()) for (const socket of table.values()) socket.close();
29113
+ sockets.clear();
28825
29114
  channel?.port1.close();
28826
29115
  channel = null;
28827
29116
  await registration.unregister();