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 +53 -33
- package/dist/index.cjs +314 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -2
- package/dist/index.d.ts +61 -2
- package/dist/index.js +313 -14
- package/dist/index.js.map +1 -1
- package/dist/integrations/server.cjs +24 -0
- package/dist/integrations/server.cjs.map +1 -0
- package/dist/integrations/server.d.cts +2 -0
- package/dist/integrations/server.d.ts +2 -0
- package/dist/integrations/server.js +3 -0
- package/dist/integrations/server.js.map +1 -0
- package/dist/integrations/vite.cjs +22 -0
- package/dist/integrations/vite.cjs.map +1 -0
- package/dist/integrations/vite.d.cts +1 -0
- package/dist/integrations/vite.d.ts +1 -0
- package/dist/integrations/vite.js +3 -0
- package/dist/integrations/vite.js.map +1 -0
- package/package.json +14 -3
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** |
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
**
|
|
583
|
-
`worker_threads`
|
|
584
|
-
|
|
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
|
|
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");
|
|
592
|
+
await box.exec("node app/index.js");
|
|
597
593
|
```
|
|
598
594
|
|
|
599
|
-
**
|
|
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.
|
|
616
|
-
|
|
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
|
-
|
|
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
|
|
640
|
+
import { configurePython, createContainer } from "sandboxedjs";
|
|
634
641
|
|
|
635
|
-
|
|
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
|
-
|
|
639
|
-
|
|
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
|
-
|
|
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
|
|
649
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
22784
|
-
} else if (
|
|
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" &&
|
|
23095
|
+
} else if (node2.type === "ClassDeclaration" && isNode3(node2.id)) {
|
|
22809
23096
|
names.add(node2.id.name);
|
|
22810
|
-
} else if (node2.type === "FunctionDeclaration" &&
|
|
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 (!
|
|
23108
|
+
if (!isNode3(nodes)) return;
|
|
22822
23109
|
const node2 = nodes;
|
|
22823
23110
|
if (node2.type === "FunctionDeclaration" || node2.type === "FunctionExpression" || node2.type === "ArrowFunctionExpression") {
|
|
22824
|
-
if (
|
|
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 (!
|
|
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
|
|
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
|
|
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;
|