sandboxedjs 0.1.14 → 0.1.16

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
@@ -579,33 +579,87 @@ 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. In a browser you boot Nodepod's browser build yourself it needs a
584
- service worker, per Nodepod's own setup docs — and hand the 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:
585
585
 
586
586
  ```ts
587
- import { Nodepod } from "@scelar/nodepod";
588
587
  import { createContainer } from "sandboxedjs";
589
588
 
590
- const pod = await Nodepod.boot({ /* your service-worker setup */ });
591
- const box = await createContainer({ pod });
589
+ const box = await createContainer({ files: { "/app/index.js": "console.log(1)" } });
592
590
 
593
591
  await box.exec("ls -la /"); // shell + coreutils
594
- await box.exec("node app.js"); // Node, through Nodepod's browser engine
592
+ await box.exec("node app/index.js");
595
593
  ```
596
594
 
597
- **What will not work in a browser:**
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:
613
+
614
+ ```
615
+ $ uname -a Linux sandbox 5.10.0 (sandboxedjs@nodepod) … x86_64 GNU/Linux
616
+ $ ls -la /app the mounted files
617
+ $ grep beta … beta
618
+ $ node /app/hello.js node runtime says hi
619
+ ```
620
+
621
+ Shell, coreutils, the virtual filesystem and the Node runtime all work. Boot costs about ten
622
+ seconds, nearly all of it Nodepod starting.
623
+
624
+ **What does not work in a browser:**
598
625
 
599
626
  - `copyIn()` / `copyOut()` — they read and write the host filesystem, which does not exist.
600
- - `expose()` — it opens a real `node:http` listener. In a browser you use Nodepod's service
601
- 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.
629
+ - `python3` — see below.
602
630
  - The `sandboxedjs` CLI, obviously.
603
631
 
604
- Those three use dynamic imports, so they only fail if you call them.
632
+ Those use dynamic imports, so they only fail if you call them.
633
+
634
+ ### Python in a browser
635
+
636
+ MicroPython does not currently start in a browser. Two separate things are involved.
637
+
638
+ The first is ours and is fixed: Emscripten resolves `micropython.wasm` against the loader's own
639
+ directory, which is right in Node and wrong once a bundler has rewritten the loader into a hashed
640
+ chunk — the guess lands on a path the dev server answers with `index.html`, and instantiation
641
+ dies on `expected magic word 00 61 73 6d, found 3c 21 64 6f` (the first bytes of `<!doctype`).
642
+ Pass the real URL and that goes away:
643
+
644
+ ```ts
645
+ import wasm from "@micropython/micropython-webassembly-pyscript/micropython.wasm?url";
646
+
647
+ await createContainer({ pod, python: { wasmUrl: wasm } });
648
+ ```
649
+
650
+ `?url` is Vite's spelling; every bundler has its own. `configurePython({ wasmUrl })` does the
651
+ same thing outside a container.
652
+
653
+ The second is upstream and is not fixed. With the correct binary loaded, instantiation fails on
654
+
655
+ ```
656
+ LinkError: Import #45 "env" "__syscall_poll_nonblocking": function import requires a callable
657
+ ```
605
658
 
606
- **Not yet verified.** I have not run this in a browser end to end. The Node-side blockers are
607
- removed and the seam is there, but treat browser support as "should work, unproven" rather than
608
- a tested claim.
659
+ The symbol appears nowhere in the shipped `micropython.mjs`, so the published glue and `.wasm`
660
+ disagree about their import table. Node is unaffected the Python tests pass there and nothing
661
+ in this package can supply a symbol the loader never declares. Treat `python3` as Node-only until
662
+ the upstream package ships a matching pair.
609
663
 
610
664
  ## Uploading files
611
665
 
package/dist/index.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var headless = require('@scelar/nodepod/headless');
5
+ var nodepod = require('@scelar/nodepod');
6
6
  var acorn = require('acorn');
7
7
 
8
8
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
@@ -8553,6 +8553,15 @@ async function nodeBuiltin(name) {
8553
8553
  specifier
8554
8554
  );
8555
8555
  }
8556
+ async function nodeOnlyModule(specifier) {
8557
+ const parts = specifier.split("/");
8558
+ const runtimeSpecifier = parts.join("/");
8559
+ return await import(
8560
+ /* @vite-ignore */
8561
+ /* webpackIgnore: true */
8562
+ runtimeSpecifier
8563
+ );
8564
+ }
8556
8565
  var zlibPromise = null;
8557
8566
  function nodeZlib() {
8558
8567
  zlibPromise ??= nodeBuiltin("zlib");
@@ -8755,7 +8764,7 @@ function installEsbuildRuntime() {
8755
8764
  async function install() {
8756
8765
  const workerPath = await buildPatchedWorker();
8757
8766
  if (!workerPath) return;
8758
- const { createNodeHost, setRuntimeHost } = await import('@scelar/nodepod/headless');
8767
+ const { createNodeHost, setRuntimeHost } = await nodeOnlyModule("@scelar/nodepod/headless");
8759
8768
  setRuntimeHost(createNodeHost({ workerPath }));
8760
8769
  }
8761
8770
  async function buildPatchedWorker() {
@@ -22144,6 +22153,10 @@ init_path();
22144
22153
  var PYTHON_VERSION = "3.4.0";
22145
22154
  var MICROPYTHON_BANNER = "MicroPython v1.28.0 on 2026-04-06; SandboxedJS with Emscripten";
22146
22155
  var loaderPromise = null;
22156
+ var wasmUrl;
22157
+ function configurePython(options = {}) {
22158
+ if (options.wasmUrl !== void 0) wasmUrl = options.wasmUrl;
22159
+ }
22147
22160
  async function getLoader() {
22148
22161
  if (!loaderPromise) {
22149
22162
  loaderPromise = import('@micropython/micropython-webassembly-pyscript/micropython.mjs').then(
@@ -22166,6 +22179,7 @@ async function createInterpreter(ctx, opts) {
22166
22179
  stdout: opts.stdout,
22167
22180
  stderr: opts.stderr,
22168
22181
  ...opts.stdin ? { stdin: opts.stdin } : {},
22182
+ ...wasmUrl ? { url: wasmUrl } : {},
22169
22183
  linebuffer: false,
22170
22184
  heapsize: opts.heapsize ?? 64 * 1024 * 1024
22171
22185
  });
@@ -22890,7 +22904,7 @@ function splitPackageSpec(spec) {
22890
22904
  return { name: spec.slice(0, at), version: spec.slice(at + 1) };
22891
22905
  }
22892
22906
  async function installPackages(ctx, specs, opts) {
22893
- const installer = opts.cwd === "/" ? ctx.kernel.pod.packages : new headless.DependencyInstaller(ctx.vfs.volume, { cwd: opts.cwd });
22907
+ const installer = opts.cwd === "/" ? ctx.kernel.pod.packages : new nodepod.DependencyInstaller(ctx.vfs.volume, { cwd: opts.cwd });
22894
22908
  const onProgress = (message) => {
22895
22909
  if (!opts.quiet) ctx.stderr.write(message.endsWith("\n") ? message : message + "\n");
22896
22910
  };
@@ -23182,6 +23196,12 @@ function packageBinaries(ctx, root, packageName) {
23182
23196
  }
23183
23197
  return [];
23184
23198
  }
23199
+ var CLI_MOVED_TO = {
23200
+ tailwindcss: "@tailwindcss/cli",
23201
+ postcss: "postcss-cli",
23202
+ autoprefixer: "postcss-cli",
23203
+ sass: "sass-embedded"
23204
+ };
23185
23205
  function findLocalBin(ctx, name) {
23186
23206
  let dir3 = ctx.cwd;
23187
23207
  for (let i = 0; i < 64; i++) {
@@ -23288,6 +23308,14 @@ unless the container was created with network: { allowOutbound: true }.`,
23288
23308
  const chosen = binaries.includes(command) ? command : binaries[0];
23289
23309
  if (chosen === void 0) {
23290
23310
  ctx.warn(`could not determine executable to run: ${packageName} provides no binary`);
23311
+ const replacement = CLI_MOVED_TO[packageName];
23312
+ if (replacement !== void 0) {
23313
+ ctx.warn(`'${packageName}' ships no CLI; its command lives in '${replacement}'.`);
23314
+ ctx.warn(`Try: npx ${replacement} ${rest.join(" ")}`.trimEnd());
23315
+ } else {
23316
+ ctx.warn(`'${packageName}' is installed but declares no "bin" entry.`);
23317
+ ctx.warn("Check which package provides the command, or use `npx -p <pkg> <command>`.");
23318
+ }
23291
23319
  return 127;
23292
23320
  }
23293
23321
  command = chosen;
@@ -23864,14 +23892,9 @@ var Container = class _Container {
23864
23892
  }
23865
23893
  // ── boot ──────────────────────────────────────────────────────────────────
23866
23894
  static async create(opts = {}) {
23867
- if (!opts.pod) await installEsbuildRuntime();
23868
- const pod = opts.pod ?? await headless.Nodepod.boot({
23869
- headless: true,
23870
- serviceWorker: false,
23871
- env: opts.env ?? {},
23872
- workdir: opts.cwd ?? "/",
23873
- ...opts.onServerReady ? { onServerReady: opts.onServerReady } : {}
23874
- });
23895
+ if (opts.python) configurePython(opts.python);
23896
+ if (!opts.pod && isNodeRuntime()) await installEsbuildRuntime();
23897
+ const pod = opts.pod ?? (isNodeRuntime() ? await bootHeadlessPod(opts) : await bootBrowserPod(opts));
23875
23898
  const kernel = new Kernel({
23876
23899
  pod,
23877
23900
  hostname: opts.hostname ?? "sandbox",
@@ -24293,6 +24316,30 @@ function normalizeHeaders(headers) {
24293
24316
  async function createContainer(opts = {}) {
24294
24317
  return Container.create(opts);
24295
24318
  }
24319
+ function isNodeRuntime() {
24320
+ return typeof process !== "undefined" && process.versions != null && process.versions.node != null;
24321
+ }
24322
+ async function bootBrowserPod(opts) {
24323
+ const { Nodepod } = await import('@scelar/nodepod');
24324
+ return Nodepod.boot({
24325
+ env: opts.env ?? {},
24326
+ workdir: opts.cwd ?? "/",
24327
+ ...opts.browser ?? {},
24328
+ ...opts.onServerReady ? { onServerReady: opts.onServerReady } : {}
24329
+ });
24330
+ }
24331
+ async function bootHeadlessPod(opts) {
24332
+ const { Nodepod } = await nodeOnlyModule(
24333
+ "@scelar/nodepod/headless"
24334
+ );
24335
+ return Nodepod.boot({
24336
+ headless: true,
24337
+ serviceWorker: false,
24338
+ env: opts.env ?? {},
24339
+ workdir: opts.cwd ?? "/",
24340
+ ...opts.onServerReady ? { onServerReady: opts.onServerReady } : {}
24341
+ });
24342
+ }
24296
24343
 
24297
24344
  // src/container/terminal.ts
24298
24345
  init_path();
@@ -24771,6 +24818,7 @@ exports.braceExpand = braceExpand;
24771
24818
  exports.buildRootfs = buildRootfs;
24772
24819
  exports.builtinNames = builtinNames;
24773
24820
  exports.captureStdio = captureStdio;
24821
+ exports.configurePython = configurePython;
24774
24822
  exports.createContainer = createContainer;
24775
24823
  exports.createContext = createContext;
24776
24824
  exports.default = src_default;