sandboxedjs 0.1.15 → 0.1.17

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/README.md CHANGED
@@ -44,7 +44,7 @@ Node 18.17+. Everything is pure JavaScript and WebAssembly — no compilation st
44
44
  | **Shell** | POSIX `sh` (54 builtins) — pipelines, redirection, here-docs, globbing, brace/parameter/arithmetic/command expansion, functions, `if`/`for`/`while`/`case`/`select`, job control, traps, arrays, `[[ ]]`, `(( ))` |
45
45
  | **Coreutils** | 154 programs — `ls cat cp mv rm mkdir grep sed awk find head tail sort uniq wc cut tr tee xargs chmod chown ln du df ps tar gzip base64 sha256sum diff curl wget` and the rest — plus 54 shell builtins |
46
46
  | **Node.js** | Real Node semantics via Nodepod — `require`, npm packages, `http`, `fs`, streams, `worker_threads` |
47
- | **Python** | MicroPython on WebAssembly, mounted on the *same* filesystem |
47
+ | **Python** | CPython 3.13 (Pyodide) on the *same* filesystem, with the real standard library |
48
48
  | **FFmpeg** | `ffmpeg` and `ffprobe` (FFmpeg 5.1) reading and writing container files directly — [optional install](#video-and-audio) |
49
49
  | **`/proc`** | Live and synthesised — `ps`, `top`, `free` and `uptime` all read the same source |
50
50
  | **Networking** | Virtual interfaces, `/etc/hosts` resolution, in-container HTTP servers, an optional bridge to a real host port |
@@ -404,7 +404,7 @@ your disk.
404
404
 
405
405
  ### Python
406
406
 
407
- Python is MicroPython compiled to WebAssembly, mounted on the container's filesystem:
407
+ Python is CPython 3.13, via Pyodide, mounted on the container's filesystem:
408
408
 
409
409
  ```ts
410
410
  await box.exec("python3 -c \"print(open('/etc/hostname').read())\"");
@@ -414,7 +414,7 @@ await box.exec("echo '1 2 3' | python3 -c \"import sys; print(sum(map(int, sys.s
414
414
 
415
415
  The bundled standard library includes `json`, `re`, `os`, `sys`, `math`, `random`, `hashlib`,
416
416
  `binascii`, `struct`, `time`, `collections`, `itertools`, `functools`, `asyncio` and more. It is
417
- MicroPython, not CPython there is no `numpy`, and `pip` can install pure-Python wheels only
417
+ Real CPython, so `sqlite3`, `dataclasses`, `decimal` and `typing` all work. Packages must be built for WebAssembly: `micropip` installs pure-Python wheels, and Pyodide's own distribution covers the rest
418
418
  (and only with outbound networking enabled).
419
419
 
420
420
  ### Filesystem from the host
@@ -478,7 +478,7 @@ line is measured on the spot rather than claimed, so a runtime that is missing s
478
478
  sandboxedjs — a Linux-like container inside Node.js
479
479
 
480
480
  node v22.12.0 npm, require, http servers
481
- python3 MicroPython no C extensions (numpy, pandas)
481
+ python3 CPython 3.13 only WebAssembly-built C extensions
482
482
  ffmpeg 5.1.4 video and audio
483
483
  network on npm install reaches the real registry
484
484
  ```
@@ -579,29 +579,41 @@ resolve their implementation at call time (`node:zlib`/`node:crypto` on Node,
579
579
  `CompressionStream`/`crypto.subtle` plus a JS MD5 in a browser), so nothing pulls a `node:`
580
580
  builtin in when the module loads and a bundler will not fail on it.
581
581
 
582
- **You supply the pod.** The default boot path uses `@scelar/nodepod/headless`, which installs a
583
- `worker_threads` host. That entry is now loaded lazily, behind a runtime-built specifier, so a
584
- browser bundle never pulls `node:worker_threads` in through it. In a browser you boot Nodepod's
585
- browser build yourself — it needs a service worker, per Nodepod's own setup docs — and hand the
586
- instance over:
582
+ **One call, either side.** `createContainer()` picks its own host: the headless
583
+ `worker_threads` engine on Node, Nodepod's browser build in a browser. You do not import Nodepod,
584
+ and you do not pass a pod:
587
585
 
588
586
  ```ts
589
- import { Nodepod } from "@scelar/nodepod";
590
587
  import { createContainer } from "sandboxedjs";
591
588
 
592
- const pod = await Nodepod.boot({ /* your service-worker setup */ });
593
- const box = await createContainer({ pod });
589
+ const box = await createContainer({ files: { "/app/index.js": "console.log(1)" } });
594
590
 
595
591
  await box.exec("ls -la /"); // shell + coreutils
596
- await box.exec("node app.js"); // Node, through Nodepod's browser engine
592
+ await box.exec("node app/index.js");
597
593
  ```
598
594
 
599
- **Verified in a browser.** Booted under Vite with Nodepod's browser build, the following run:
595
+ **Serve the service worker.** In a browser, a container's preview iframe reaches an HTTP server
596
+ running inside the page through a service worker, and a browser will not register one from
597
+ `node_modules` — it has to come from your own origin at `/__sw__.js`. One line:
598
+
599
+ ```ts
600
+ // vite.config.ts
601
+ import sandboxedjs from "sandboxedjs/vite";
602
+ export default defineConfig({ plugins: [sandboxedjs()] });
603
+ ```
604
+
605
+ For anything else, `sandboxedjs/server` exports `serveSW()` (Fetch-style hosts) and
606
+ `serveSWNode()` (Express, Fastify, `node:http`). Skip it with
607
+ `createContainer({ browser: { serviceWorker: false } })`, which also disables preview iframes.
608
+
609
+ You can still boot the pod yourself and pass it as `pod` when you need control over Nodepod's
610
+ own options.
611
+
612
+ **Verified in a browser.** Booted under Vite, the following run:
600
613
 
601
614
  ```
602
615
  $ uname -a Linux sandbox 5.10.0 (sandboxedjs@nodepod) … x86_64 GNU/Linux
603
616
  $ ls -la /app the mounted files
604
- $ wc -l /app/data.txt 3 /app/data.txt
605
617
  $ grep beta … beta
606
618
  $ node /app/hello.js node runtime says hi
607
619
  ```
@@ -612,8 +624,8 @@ seconds, nearly all of it Nodepod starting.
612
624
  **What does not work in a browser:**
613
625
 
614
626
  - `copyIn()` / `copyOut()` — they read and write the host filesystem, which does not exist.
615
- - `expose()` — it opens a real `node:http` listener. In a browser you use Nodepod's service
616
- worker and preview iframe to reach an in-container server instead of a host port.
627
+ - `expose()` — it opens a real `node:http` listener. Use the service worker and a preview iframe
628
+ to reach an in-container server instead of a host port.
617
629
  - `python3` — see below.
618
630
  - The `sandboxedjs` CLI, obviously.
619
631
 
@@ -621,33 +633,35 @@ Those use dynamic imports, so they only fail if you call them.
621
633
 
622
634
  ### Python in a browser
623
635
 
624
- MicroPython does not currently start in a browser. Two separate things are involved.
625
-
626
- The first is ours and is fixed: Emscripten resolves `micropython.wasm` against the loader's own
627
- directory, which is right in Node and wrong once a bundler has rewritten the loader into a hashed
628
- chunk — the guess lands on a path the dev server answers with `index.html`, and instantiation
629
- dies on `expected magic word 00 61 73 6d, found 3c 21 64 6f` (the first bytes of `<!doctype`).
630
- Pass the real URL and that goes away:
636
+ Node resolves Pyodide from the installed package. A browser cannot resolve it by name, so tell it
637
+ where the module and the assets are — a CDN is fine:
631
638
 
632
639
  ```ts
633
- import wasm from "@micropython/micropython-webassembly-pyscript/micropython.wasm?url";
640
+ import { configurePython, createContainer } from "sandboxedjs";
634
641
 
635
- await createContainer({ pod, python: { wasmUrl: wasm } });
642
+ configurePython({
643
+ engine: "cpython",
644
+ pyodideURL: "https://cdn.jsdelivr.net/pyodide/v0.28.3/full/pyodide.mjs",
645
+ indexURL: "https://cdn.jsdelivr.net/pyodide/v0.28.3/full/",
646
+ });
647
+
648
+ const box = await createContainer();
649
+ await box.exec("python3 -c 'import sqlite3; print(sqlite3.sqlite_version)'");
636
650
  ```
637
651
 
638
- `?url` is Vite's spelling; every bundler has its own. `configurePython({ wasmUrl })` does the
639
- same thing outside a container.
652
+ Without that, `engine: "auto"` finds no interpreter and falls back to MicroPython, which is why a
653
+ browser build of this package carries no Python runtime it was not asked for.
640
654
 
641
- The second is upstream and is not fixed. With the correct binary loaded, instantiation fails on
655
+ **MicroPython in a browser still does not start**, and that part is upstream. With the correct
656
+ `.wasm` loaded — see `configurePython({ wasmUrl })` — instantiation fails on
642
657
 
643
658
  ```
644
659
  LinkError: Import #45 "env" "__syscall_poll_nonblocking": function import requires a callable
645
660
  ```
646
661
 
647
662
  The symbol appears nowhere in the shipped `micropython.mjs`, so the published glue and `.wasm`
648
- disagree about their import table. Node is unaffected the Python tests pass there — and nothing
649
- in this package can supply a symbol the loader never declares. Treat `python3` as Node-only until
650
- the upstream package ships a matching pair.
663
+ disagree about their import table. Node is unaffected. Since CPython is now the default engine,
664
+ this only matters if you have deliberately pinned `engine: "micropython"` in a browser.
651
665
 
652
666
  ## Uploading files
653
667
 
@@ -732,7 +746,13 @@ Honest list of what does not work:
732
746
  This affects older toolchains — Vite 4 and its contemporaries. Current Vite ships its Rust
733
747
  pipeline as WebAssembly and runs fine, dev server included, as do Express, Koa, Fastify-style
734
748
  apps and plain `http` servers.
735
- - **Python is MicroPython**, so C extensions (`numpy`, `pandas`, `cryptography`) are unavailable.
749
+ - **Python is CPython via Pyodide.** The standard library is the real one. A C
750
+ extension works only if it has been built for WebAssembly — Pyodide ships
751
+ many, including `numpy`, but an arbitrary wheel from PyPI will not install.
752
+ Starting an interpreter costs about a second and a half; one is kept per
753
+ container, and each program runs in its own namespace.
754
+ - **MicroPython is still there** for hosts that would rather not carry a 13 MB
755
+ interpreter: `configurePython({ engine: "micropython" })`.
736
756
  - **No real sockets.** HTTP servers work through the request proxy; raw TCP/UDP does not.
737
757
  - **No real processes.** Processes are cooperative async tasks: `kill -9` cannot interrupt a
738
758
  tight synchronous loop, and `SIGSTOP` only marks state.
package/dist/index.cjs CHANGED
@@ -21941,6 +21941,7 @@ function mountContainerFs(FS, opts) {
21941
21941
  if (isSysError(e)) return throwErrno(e.code);
21942
21942
  throw new FS.ErrnoError(emErrno("EIO"));
21943
21943
  };
21944
+ const mountRoot = opts.mountAt ? normalize(opts.mountAt) : "/";
21944
21945
  const backend = {
21945
21946
  /** Absolute container path for a node. */
21946
21947
  realPath(node2) {
@@ -21950,7 +21951,9 @@ function mountContainerFs(FS, opts) {
21950
21951
  parts.unshift(current.name);
21951
21952
  current = current.parent;
21952
21953
  }
21953
- return "/" + parts.join("/");
21954
+ const relative2 = parts.join("/");
21955
+ if (mountRoot === "/") return "/" + relative2;
21956
+ return relative2 ? join(mountRoot, relative2) : mountRoot;
21954
21957
  },
21955
21958
  mount(_mount) {
21956
21959
  return backend.createNode(null, "/", 16877 | 0, 0);
@@ -22140,6 +22143,14 @@ function mountContainerFs(FS, opts) {
22140
22143
  return position;
22141
22144
  }
22142
22145
  };
22146
+ if (opts.mountAt) {
22147
+ try {
22148
+ FS.mkdir(mountRoot);
22149
+ } catch {
22150
+ }
22151
+ FS.mount(backend, {}, mountRoot);
22152
+ return;
22153
+ }
22143
22154
  FS.root = null;
22144
22155
  FS.mount(backend, {}, "/");
22145
22156
  try {
@@ -22148,13 +22159,286 @@ function mountContainerFs(FS, opts) {
22148
22159
  }
22149
22160
  }
22150
22161
 
22162
+ // src/runtime/cpython.ts
22163
+ var RESERVED_FOR_INTERPRETER = /* @__PURE__ */ new Set(["lib", "dev", "proc"]);
22164
+ var pyodideModule = null;
22165
+ var indexUrl;
22166
+ var moduleUrl;
22167
+ function configureCPython(options = {}) {
22168
+ if (options.indexURL !== void 0) indexUrl = options.indexURL;
22169
+ if (options.moduleURL !== void 0) {
22170
+ moduleUrl = options.moduleURL;
22171
+ pyodideModule = null;
22172
+ }
22173
+ }
22174
+ function importPyodide() {
22175
+ if (!pyodideModule) {
22176
+ const specifier = moduleUrl ?? ["py", "odide"].join("");
22177
+ pyodideModule = import(
22178
+ /* @vite-ignore */
22179
+ /* webpackIgnore: true */
22180
+ specifier
22181
+ );
22182
+ }
22183
+ return pyodideModule;
22184
+ }
22185
+ var isNode2 = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
22186
+ async function resolveIndexUrl() {
22187
+ if (indexUrl) return indexUrl;
22188
+ if (!isNode2) return void 0;
22189
+ try {
22190
+ const { createRequire } = await nodeBuiltin("module");
22191
+ const path = await nodeBuiltin("path");
22192
+ const require2 = createRequire(path.join(process.cwd(), "index.js"));
22193
+ return `${path.dirname(require2.resolve("pyodide/package.json"))}/`;
22194
+ } catch {
22195
+ return void 0;
22196
+ }
22197
+ }
22198
+ async function isCPythonAvailable() {
22199
+ try {
22200
+ await importPyodide();
22201
+ return true;
22202
+ } catch {
22203
+ return false;
22204
+ }
22205
+ }
22206
+ var interpreters = /* @__PURE__ */ new WeakMap();
22207
+ async function interpreterFor(ctx) {
22208
+ const existing = interpreters.get(ctx.vfs);
22209
+ if (existing) return existing;
22210
+ const starting = (async () => {
22211
+ const { loadPyodide } = await importPyodide();
22212
+ const resolved = await resolveIndexUrl();
22213
+ const py = await loadPyodide({
22214
+ ...resolved ? { indexURL: resolved } : {},
22215
+ /* Output is rebound per program; these only catch anything printed
22216
+ * while the interpreter is still starting. */
22217
+ stdout: () => {
22218
+ },
22219
+ stderr: () => {
22220
+ }
22221
+ });
22222
+ mountContainerDirs(py, ctx);
22223
+ installRunner(py);
22224
+ return py;
22225
+ })();
22226
+ interpreters.set(ctx.vfs, starting);
22227
+ try {
22228
+ return await starting;
22229
+ } catch (error) {
22230
+ interpreters.delete(ctx.vfs);
22231
+ throw error;
22232
+ }
22233
+ }
22234
+ function installRunner(py) {
22235
+ py.__sbxRun = py.runPython(`
22236
+ from pyodide.code import eval_code_async
22237
+
22238
+ async def __sbx_run(source, scope):
22239
+ try:
22240
+ await eval_code_async(source, globals=scope)
22241
+ return 0
22242
+ except SystemExit as exit:
22243
+ code = exit.code
22244
+ if code is None:
22245
+ return 0
22246
+ return code if isinstance(code, int) else 1
22247
+
22248
+ __sbx_run
22249
+ `);
22250
+ }
22251
+ function mountContainerDirs(py, ctx) {
22252
+ const mounted = py.__sbxMounted ??= /* @__PURE__ */ new Set();
22253
+ let entries;
22254
+ try {
22255
+ entries = ctx.vfs.readdirWithTypes("/", ctx.cred);
22256
+ } catch {
22257
+ return;
22258
+ }
22259
+ for (const entry of entries) {
22260
+ if (entry.kind !== "directory") continue;
22261
+ if (RESERVED_FOR_INTERPRETER.has(entry.name)) continue;
22262
+ if (mounted.has(entry.name)) continue;
22263
+ try {
22264
+ mountContainerFs(py.FS, {
22265
+ vfs: ctx.vfs,
22266
+ cred: ctx.cred,
22267
+ mountAt: `/${entry.name}`
22268
+ });
22269
+ mounted.add(entry.name);
22270
+ } catch {
22271
+ }
22272
+ }
22273
+ }
22274
+ function bootstrap(py, ctx, argv, scriptDir) {
22275
+ const paths = [
22276
+ ...scriptDir ? [scriptDir] : [""],
22277
+ "/usr/lib/python3",
22278
+ "/usr/lib/python3/site-packages",
22279
+ "/usr/local/lib/python3/site-packages"
22280
+ ];
22281
+ py.runPython(`
22282
+ import sys, os
22283
+ sys.argv[:] = ${JSON.stringify(argv)}
22284
+ for __p in reversed(${JSON.stringify(paths)}):
22285
+ if __p and __p not in sys.path:
22286
+ sys.path.insert(0, __p)
22287
+ elif __p == "" and "" not in sys.path:
22288
+ sys.path.insert(0, "")
22289
+ try:
22290
+ del __p
22291
+ except NameError:
22292
+ pass
22293
+ os.environ.clear()
22294
+ os.environ.update(${JSON.stringify(ctx.env)})
22295
+ `);
22296
+ try {
22297
+ py.FS.chdir(ctx.cwd);
22298
+ } catch {
22299
+ }
22300
+ }
22301
+ function reportError(ctx, error) {
22302
+ const message = error instanceof Error ? error.message : String(error);
22303
+ const systemExit = /SystemExit:?\s*(-?\d+)?/.exec(message);
22304
+ if (systemExit) {
22305
+ return systemExit[1] !== void 0 ? Number(systemExit[1]) & 255 : 0;
22306
+ }
22307
+ if (/KeyboardInterrupt/.test(message)) {
22308
+ ctx.stderr.write("KeyboardInterrupt\n");
22309
+ return 130;
22310
+ }
22311
+ const text = message.replace(/^PythonError:\s*/, "");
22312
+ ctx.stderr.write(text.endsWith("\n") ? text : `${text}
22313
+ `);
22314
+ return 1;
22315
+ }
22316
+ async function runCPythonProgram(ctx, source, argv, scriptDir, stdinText) {
22317
+ let py;
22318
+ try {
22319
+ py = await interpreterFor(ctx);
22320
+ } catch (error) {
22321
+ ctx.stderr.write(
22322
+ `python3: CPython is unavailable in this host (${error instanceof Error ? error.message : String(error)})
22323
+ `
22324
+ );
22325
+ return { exitCode: 127 };
22326
+ }
22327
+ mountContainerDirs(py, ctx);
22328
+ try {
22329
+ await py.loadPackagesFromImports(source, {
22330
+ messageCallback: () => {
22331
+ },
22332
+ errorCallback: () => {
22333
+ }
22334
+ });
22335
+ } catch {
22336
+ }
22337
+ const encoder6 = new TextEncoder();
22338
+ const decoder7 = new TextDecoder();
22339
+ py.setStdout({
22340
+ write: (buffer) => {
22341
+ ctx.write(decoder7.decode(buffer));
22342
+ return buffer.length;
22343
+ }
22344
+ });
22345
+ py.setStderr({
22346
+ write: (buffer) => {
22347
+ ctx.stderr.write(decoder7.decode(buffer));
22348
+ return buffer.length;
22349
+ }
22350
+ });
22351
+ if (stdinText !== null) {
22352
+ const bytes = encoder6.encode(stdinText);
22353
+ let offset = 0;
22354
+ py.setStdin({
22355
+ read: (buffer) => {
22356
+ const take = Math.min(buffer.length, bytes.length - offset);
22357
+ buffer.set(bytes.subarray(offset, offset + take));
22358
+ offset += take;
22359
+ return take;
22360
+ }
22361
+ });
22362
+ } else {
22363
+ py.setStdin({ read: () => 0 });
22364
+ }
22365
+ try {
22366
+ bootstrap(py, ctx, argv, scriptDir);
22367
+ } catch (error) {
22368
+ return { exitCode: reportError(ctx, error) };
22369
+ }
22370
+ const globals = py.globals.get("dict")();
22371
+ try {
22372
+ globals.set("__name__", "__main__");
22373
+ const exitCode = await py.__sbxRun(source, globals);
22374
+ return { exitCode: Number(exitCode) & 255 };
22375
+ } catch (error) {
22376
+ return { exitCode: reportError(ctx, error) };
22377
+ } finally {
22378
+ try {
22379
+ globals.destroy();
22380
+ } catch {
22381
+ }
22382
+ }
22383
+ }
22384
+ var micropip = defineCommand({
22385
+ name: "micropip",
22386
+ path: "/usr/bin/micropip",
22387
+ summary: "install Python packages into the running interpreter",
22388
+ usage: "micropip install <package>...",
22389
+ async run(ctx) {
22390
+ const [action, ...packages] = ctx.args;
22391
+ if (action !== "install" || packages.length === 0) {
22392
+ ctx.line("usage: micropip install <package>...");
22393
+ return action === void 0 ? 1 : 0;
22394
+ }
22395
+ let py;
22396
+ try {
22397
+ py = await interpreterFor(ctx);
22398
+ } catch {
22399
+ ctx.stderr.write("micropip: CPython is unavailable in this host\n");
22400
+ return 127;
22401
+ }
22402
+ try {
22403
+ await py.loadPackage("micropip");
22404
+ const micropipModule = py.pyimport("micropip");
22405
+ for (const name of packages) {
22406
+ ctx.line(`Collecting ${name}`);
22407
+ await micropipModule.install(name);
22408
+ ctx.line(`Successfully installed ${name}`);
22409
+ }
22410
+ return 0;
22411
+ } catch (error) {
22412
+ ctx.stderr.write(
22413
+ `micropip: ${error instanceof Error ? error.message : String(error)}
22414
+ `
22415
+ );
22416
+ return 1;
22417
+ }
22418
+ }
22419
+ });
22420
+
22151
22421
  // src/runtime/python.ts
22152
22422
  init_path();
22153
22423
  var PYTHON_VERSION = "3.4.0";
22154
22424
  var MICROPYTHON_BANNER = "MicroPython v1.28.0 on 2026-04-06; SandboxedJS with Emscripten";
22155
22425
  var loaderPromise = null;
22156
22426
  var wasmUrl;
22427
+ var engine = "auto";
22428
+ var cpythonUsable = null;
22429
+ async function useCPython() {
22430
+ if (engine === "micropython") return false;
22431
+ if (engine === "cpython") return true;
22432
+ cpythonUsable ??= isCPythonAvailable();
22433
+ return cpythonUsable;
22434
+ }
22157
22435
  function configurePython(options = {}) {
22436
+ if (options.engine !== void 0) {
22437
+ engine = options.engine;
22438
+ cpythonUsable = null;
22439
+ }
22440
+ if (options.indexURL !== void 0) configureCPython({ indexURL: options.indexURL });
22441
+ if (options.pyodideURL !== void 0) configureCPython({ moduleURL: options.pyodideURL });
22158
22442
  if (options.wasmUrl !== void 0) wasmUrl = options.wasmUrl;
22159
22443
  }
22160
22444
  async function getLoader() {
@@ -22229,6 +22513,9 @@ function reportPythonError(ctx, e) {
22229
22513
  return 1;
22230
22514
  }
22231
22515
  async function runProgram(ctx, source, argv, scriptDir, stdinText) {
22516
+ if (await useCPython()) {
22517
+ return runCPythonProgram(ctx, source, argv, scriptDir, stdinText);
22518
+ }
22232
22519
  let stdinOffset = 0;
22233
22520
  const stdinBytes = stdinText === null ? new Uint8Array(0) : new TextEncoder().encode(stdinText);
22234
22521
  const mp = await createInterpreter(ctx, {
@@ -22551,7 +22838,7 @@ async function readZip(data) {
22551
22838
  return entries;
22552
22839
  }
22553
22840
  function pythonCommands() {
22554
- return [python, pip];
22841
+ return [python, pip, micropip];
22555
22842
  }
22556
22843
 
22557
22844
  // src/runtime/ffmpeg.ts
@@ -22780,8 +23067,8 @@ function renameShadowedExports(source) {
22780
23067
  for (const [key, value] of Object.entries(node2)) {
22781
23068
  if (key === "type" || key === "start" || key === "end" || skip.includes(key)) continue;
22782
23069
  if (Array.isArray(value)) {
22783
- for (const item of value) if (isNode2(item)) visit(item, childScope);
22784
- } else if (isNode2(value)) {
23070
+ for (const item of value) if (isNode3(item)) visit(item, childScope);
23071
+ } else if (isNode3(value)) {
22785
23072
  visit(value, childScope);
22786
23073
  }
22787
23074
  }
@@ -22805,9 +23092,9 @@ function blockNames(body) {
22805
23092
  for (const declarator of node2.declarations) {
22806
23093
  collectPattern(declarator.id, names);
22807
23094
  }
22808
- } else if (node2.type === "ClassDeclaration" && isNode2(node2.id)) {
23095
+ } else if (node2.type === "ClassDeclaration" && isNode3(node2.id)) {
22809
23096
  names.add(node2.id.name);
22810
- } else if (node2.type === "FunctionDeclaration" && isNode2(node2.id)) {
23097
+ } else if (node2.type === "FunctionDeclaration" && isNode3(node2.id)) {
22811
23098
  names.add(node2.id.name);
22812
23099
  }
22813
23100
  }
@@ -22818,10 +23105,10 @@ function collectVars(nodes, names) {
22818
23105
  for (const item of nodes) collectVars(item, names);
22819
23106
  return;
22820
23107
  }
22821
- if (!isNode2(nodes)) return;
23108
+ if (!isNode3(nodes)) return;
22822
23109
  const node2 = nodes;
22823
23110
  if (node2.type === "FunctionDeclaration" || node2.type === "FunctionExpression" || node2.type === "ArrowFunctionExpression") {
22824
- if (isNode2(node2.id)) names.add(node2.id.name);
23111
+ if (isNode3(node2.id)) names.add(node2.id.name);
22825
23112
  return;
22826
23113
  }
22827
23114
  if (node2.type === "VariableDeclaration" && node2.kind === "var") {
@@ -22835,7 +23122,7 @@ function collectVars(nodes, names) {
22835
23122
  }
22836
23123
  }
22837
23124
  function collectPattern(node2, names) {
22838
- if (!isNode2(node2)) return;
23125
+ if (!isNode3(node2)) return;
22839
23126
  switch (node2.type) {
22840
23127
  case "Identifier":
22841
23128
  names.add(node2.name);
@@ -22859,7 +23146,7 @@ function collectPattern(node2, names) {
22859
23146
  }
22860
23147
  }
22861
23148
  function isExports(value) {
22862
- return isNode2(value) && value.type === "Identifier" && value.name === NAME;
23149
+ return isNode3(value) && value.type === "Identifier" && value.name === NAME;
22863
23150
  }
22864
23151
  function freshName(source) {
22865
23152
  let name = "__sandboxedjs_exports";
@@ -22867,7 +23154,7 @@ function freshName(source) {
22867
23154
  while (source.includes(name)) name = `__sandboxedjs_exports${++suffix}`;
22868
23155
  return name;
22869
23156
  }
22870
- function isNode2(value) {
23157
+ function isNode3(value) {
22871
23158
  return typeof value === "object" && value !== null && typeof value.type === "string";
22872
23159
  }
22873
23160
  var NOTHING = [];
@@ -23893,8 +24180,8 @@ var Container = class _Container {
23893
24180
  // ── boot ──────────────────────────────────────────────────────────────────
23894
24181
  static async create(opts = {}) {
23895
24182
  if (opts.python) configurePython(opts.python);
23896
- if (!opts.pod) await installEsbuildRuntime();
23897
- const pod = opts.pod ?? await bootHeadlessPod(opts);
24183
+ if (!opts.pod && isNodeRuntime()) await installEsbuildRuntime();
24184
+ const pod = opts.pod ?? (isNodeRuntime() ? await bootHeadlessPod(opts) : await bootBrowserPod(opts));
23898
24185
  const kernel = new Kernel({
23899
24186
  pod,
23900
24187
  hostname: opts.hostname ?? "sandbox",
@@ -24316,6 +24603,18 @@ function normalizeHeaders(headers) {
24316
24603
  async function createContainer(opts = {}) {
24317
24604
  return Container.create(opts);
24318
24605
  }
24606
+ function isNodeRuntime() {
24607
+ return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
24608
+ }
24609
+ async function bootBrowserPod(opts) {
24610
+ const { Nodepod } = await import('@scelar/nodepod');
24611
+ return Nodepod.boot({
24612
+ env: opts.env ?? {},
24613
+ workdir: opts.cwd ?? "/",
24614
+ ...opts.browser ?? {},
24615
+ ...opts.onServerReady ? { onServerReady: opts.onServerReady } : {}
24616
+ });
24617
+ }
24319
24618
  async function bootHeadlessPod(opts) {
24320
24619
  const { Nodepod } = await nodeOnlyModule(
24321
24620
  "@scelar/nodepod/headless"
@@ -24806,6 +25105,7 @@ exports.braceExpand = braceExpand;
24806
25105
  exports.buildRootfs = buildRootfs;
24807
25106
  exports.builtinNames = builtinNames;
24808
25107
  exports.captureStdio = captureStdio;
25108
+ exports.configureCPython = configureCPython;
24809
25109
  exports.configurePython = configurePython;
24810
25110
  exports.createContainer = createContainer;
24811
25111
  exports.createContext = createContext;
@@ -24824,6 +25124,7 @@ exports.globToRegex = globToRegex;
24824
25124
  exports.hasMagic = hasMagic;
24825
25125
  exports.installUserland = installUserland;
24826
25126
  exports.isBuiltinName = isBuiltinName;
25127
+ exports.isCPythonAvailable = isCPythonAvailable;
24827
25128
  exports.isPythonAvailable = isPythonAvailable;
24828
25129
  exports.isSysError = isSysError;
24829
25130
  exports.makeCred = makeCred;