npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tinysandbox/js-runtime

v0.3.1

Published

Stateless QuickJS runtime for standard WebAssembly hosts

Readme

@tinysandbox/js-runtime

A small, stateless QuickJS runtime for standard WebAssembly hosts. It has no runtime dependencies and uses the same checked-in QuickJS-ng artifact and guest glue as tinysandbox's Rust/Wasmtime js command.

The package never reads a file or calls fetch itself. Supply the wasm bytes or a compiled WebAssembly.Module explicitly, compile an engine once, and call runCode() as needed. Every call creates a new bounded linear memory, wasm instance, QuickJS runtime, and context; no guest state survives the call.

import { readFile } from "node:fs/promises";
import { createEngine } from "@tinysandbox/js-runtime";

const bytes = await readFile(new URL(
  "./node_modules/@tinysandbox/js-runtime/quickjs.wasm",
  import.meta.url,
));
const engine = await createEngine(bytes);

const result = await engine.runCode("console.log(tools.search({ query: 'hello' }))", {
  globals: {
    "tools.search": ({ query }) => ({ query, hits: 1 }),
  },
  timeoutMs: 1000,
  wasmMemoryBytes: 16 * 1024 * 1024,
  quickjsHeapBytes: 8 * 1024 * 1024,
});

Browser callers can fetch the exported quickjs.wasm asset themselves. Hosts such as Convex that compile .wasm imports use the exported subpath directly:

import { createEngine } from "@tinysandbox/js-runtime";
import quickjsModule from "@tinysandbox/js-runtime/quickjs.wasm";

const engine = await createEngine(quickjsModule);
const result = await engine.runCode("(async () => { console.log(await context.value(null)) })()", {
  globals: { "context.value": async () => await doConvexWork() },
});

Browser playground

The package includes a minimal two-pane browser example that displays guest console output, the evaluated script's return value, exit status, elapsed time, and initial/peak wasm linear memory. It deliberately ends with an expression so the example can capture that value through a synchronous custom global; the browser never evaluates the source itself.

From this directory in either a source checkout or the unpacked package:

npm run example:browser

Then open http://127.0.0.1:4173/. The default script prints that window and document are unavailable inside the guest. Each click creates the same fresh, bounded QuickJS/WASM instance as a direct runCode() call.

Cloudflare Pages

The site is entirely static. In a Cloudflare Pages Git integration, use:

| Setting | Value | | --- | --- | | Framework preset | None | | Root directory | tinysandbox-js-runtime | | Build command | npm run build:site | | Build output directory | site-dist |

No environment variables, Pages Functions, Wrangler configuration, or separate deployment workflow are required. build:site creates a clean output directory containing only the playground, runtime.js, and quickjs.wasm.

runCode(code, options) resolves to exitCode, UTF-8 stdout and stderr, and initial/peak wasm memory bytes. Its defaults are a 64 MiB wasm maximum, 32 MiB QuickJS heap, 30 second monotonic deadline, and 1 MiB each for source, serialized host responses, stdout, and stderr. A wasmMemoryBytes value below the artifact minimum of 1,245,184 bytes is rejected before instantiation. Non-page-aligned values are rounded down for the actual WebAssembly maximum.

Global names use dot-separated JavaScript identifier segments. A global's first argument is the guest value; a second context argument exposes signal, deadlineMs, remainingTimeMs(), and isCancelled(). Existing one-argument functions keep working. Guest arguments cross the boundary with JavaScript's normal JSON.stringify semantics (including its omission and coercion rules); host return values must already be strict JSON values and are validated without coercion. Invalid returns, invalid names, namespace conflicts, and runtime-global shadowing fail deterministically.

A global may be async. Returning a promise suspends the guest and returns control to the V8 event loop, so runCode() and runFile() are awaited:

const result = await engine.runCode(
  "(async () => { console.log(JSON.stringify(await tools.search({ q: 'kittens' }))) })()",
  { globals: { "tools.search": async (args) => await performSearch(args) } },
);

Every global returns a promise in the guest, so one script shape works on this runtime and on tinysandbox's Rust and Node hosts. A synchronous host global is answered inline and its promise is already settled: a microtask, not a suspension, so only the globals that need it pay for suspending. Concurrent awaits settle in completion order, and a rejected promise surfaces in the guest as a catchable error. At most 64 host calls may be outstanding at once. When all slots are occupied, another call is rejected before its host callback runs, including a synchronous callback. Promises returned by admitted callbacks remain observed even when the run is cancelled before consuming their results.

Optional filesystem capability

Pass a synchronous Vfs implementation to enable the same Buffer, fs subset, and relative/absolute CommonJS loader as tinysandbox's /bin/js:

const result = await engine.runFile("main.js", {
  vfs,
  cwd: "/app",
  argv: ["js", "main.js", "one"],
});

runFile() resolves the entry against cwd, reads it through open/readAt/close, rejects invalid UTF-8, and evaluates it with the resolved path as __filename and the stack filename. If argv is omitted, it defaults to ["js", originalPath], preserving the path string passed by the caller. Relative require() resolves from the requiring module; direct fs paths resolve from cwd.

The exported Vfs interface is deliberately small and synchronous: stat, readdir, mkdir, rename, unlink, rmdir, open, readAt, writeAt, truncate, and close, plus optional abort for discarding staged writes. A run that reaches its own end closes any remaining descriptors, whatever exit status the script chose; a run the host cut short — a timeout, an abort signal, or an exhausted output limit — calls abort when supplied, otherwise close. Handles must continue to refer to the same file after its original path is renamed, unlinked, or reused. Paths delivered to it are normalized absolute paths; handles and offsets are non-negative safe integers, and positional operations do not change the guest fd cursor. Implementations report one of the exported VfsErrno strings by throwing new VfsError(code). Quotas and storage accounting belong to the supplied implementation. The package provides no production filesystem or storage backend; the repository's TestVfs exists only under test/ for examples and conformance tests.

Omitting vfs leaves the run storage-independent: Buffer is absent and both require("fs") and file-module requests throw ERR_CAPABILITY_UNAVAILABLE before any filesystem host call. This stateless package also does not provide network access, timers, guest ESM, TypeScript transpilation, Asyncify/JSPI, or persistent isolates. The compatibility glue's fetch never reaches ambient V8 networking and fails with unavailable-host capability behavior.

Host resource budgets

hostInputBytes bounds whole-file reads and decoded guest writes (default 8 MiB). hostResponseBytes bounds serialized host responses (default 1 MiB), including JSON framing and base64 expansion. Oversized reads fail before their entire contents are copied into the host or encoded. maxOpenFiles bounds guest descriptors (default 1,024). Positional reads can return fewer bytes than requested to stay within the response budget. Paths and recursive traversal are limited to 256 components.

The monotonic timeoutMs budget covers source loading in runFile, guest execution, and synchronous host calls. A host callback already executing cannot be interrupted mid-call; further guest filesystem operations are rejected after it returns if the deadline has expired.

Host callbacks can poll context.isCancelled() or context.remainingTimeMs() to cooperate with that same monotonic deadline. Polling also refreshes context.signal; deadline events cannot fire autonomously while synchronous JavaScript blocks the event loop. deadlineMs is an approximate Unix timestamp for display. An optional options.signal rejects already-aborted runs and propagates aborts raised synchronously during callbacks. Each callback's signal also aborts when that callback returns, and its listener is removed. The upstream signal listener is removed when the run finishes. A timer on the same event loop cannot interrupt runCode() or runFile(); use a separate worker if the host needs that boundary. This package runs JavaScript only and has no jq command or jqMemoryBytes option.

The package build type-checks the implementation with strict TypeScript and emits JavaScript and declarations together. Run npm test for the shared guest corpus, independent Node filesystem comparisons, and resource-limit regressions.

Releases

The portable package keeps its own version number and releases automatically alongside the Rust and native Node packages after successful CI on main. Every eligible release increments both version lines, even when changes affect only one package. The default bump is a patch; standalone #minor / #major commit markers and Conventional Commits breaking-change markers use the same rules as the native release. A breaking change below 1.0 increments the minor.

The Release workflow's manual bump input applies to both version lines; current retries their checked-in versions. Automatic releases honor [skip release] and release commits. The workflow updates the portable manifest and lockfile together, verifies the package, and pushes the shared version commit only after all packages publish successfully. Rerunning a failed release from the same source reuses its prepared versions and skips packages already published at those versions.