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

quickjs-wasm

v0.0.5

Published

Execute javascript in a secure WebAssembly sandbox

Downloads

940

Readme

QuickJS WASM Library

This library provides a WebAssembly wrapper around the QuickJS JavaScript engine, allowing you to run JavaScript code in an isolated environment with bidirectional communication between the host and the sandboxed JavaScript.

Published on npm as quickjs-wasm:

npm install quickjs-wasm

Features

  • Run JavaScript code in a sandboxed environment
  • Compile JavaScript to bytecode for faster execution
  • Load and execute bytecode
  • Call JavaScript functions from the host
  • Call host functions from JavaScript
  • Support for asynchronous JavaScript code and Promises
  • Access JavaScript objects and properties
  • Protect the host against runaway guest code with memory limits and eval timeouts

Intended use: one sandbox per execution

This library is built for short-lived, single-use JavaScript environments: create an instance, run one untrusted script, throw the instance away.

There is deliberately no disposal API. Nothing has to be released and nothing has to be tracked: create an instance, run a script, drop it. The bindings free what they allocate as each call returns — sources, bytecode buffers, and the strings copied out to the host — so a single run can evaluate and compile as much as it likes without growing. Whatever is still held when you drop the instance goes with it.

What counts as a leak

Some values are retained for the life of an instance: an object handle you were given, the function object behind a module call, the exception path in the pending-job loop. All of them are bounded per instance and reclaimed when it is dropped. This library does not treat those as bugs. A retention that can only be observed by keeping one instance alive across many scripts is out of scope, because that is precisely the mode this library tells you not to use it in.

What is in scope is anything that scales with the work a single script does — sources, bytecode, strings crossing the boundary. Those are freed at the call boundary, because the wasm heap is a fixed ~16.5MB and is only reclaimed after the run finishes, so a run that leaks competes with itself.

Creating a fresh instance per execution is also the stronger isolation property: no state carries over between untrusted scripts — no polluted prototypes, no globals stashed by a previous run, nothing observable from one script to the next.

Instances are cheap. The wasm module is fetched and compiled once per page or process and then shared, so every createQuickJS() after the first only allocates a fresh linear memory — sharing a compiled WebAssembly.Module shares no state.

Both users of this library work that way. In a NEAR smart contract the protocol instantiates the contract wasm per call and discards it afterwards. In WebAssembly Music createQuickJS() is called once per song compilation.

If you need a long-lived JS environment shared by many scripts over time, use quickjs-emscripten instead — it manages value lifetimes explicitly.

The async model

Guest await suspends at the JavaScript level inside QuickJS: when guest code calls an async host function, QuickJS parks the guest execution as a pending promise and the wasm call returns to the host. There is no Emscripten asyncify (or any stack switching) anywhere — the wasm stack fully unwinds on every host call. The host later resumes the guest by resolving the promise via promise_callback, which also runs QuickJS's pending-job loop. This is why waitForPendingAsyncInvocations() must be awaited before reading a promise result: it drains the host-side async invocations that resume the guest.

Note that the host JS thread is blocked while wasm executes synchronous guest code — eval timeouts (see Sandboxing untrusted code) are enforced by a wall-clock check inside QuickJS's interrupt handler, not by preemption. If you need the host to stay responsive regardless of what the guest does, run the sandbox in a Worker.

Basic Usage

Creating a QuickJS Instance

import { createQuickJS } from "./quickjs.js";

// Create a QuickJS instance
const quickjs = await createQuickJS();
if (!quickjs || typeof quickjs.evalSource !== "function") {
  throw new Error("Failed to create QuickJS instance");
}

Evaluating JavaScript

// Evaluate JavaScript code directly
const result = quickjs.evalSource("42;"); // returns 42
if (result !== 42) throw new Error("Expected 42, got " + result);

Compiling and Running Bytecode

// Compile JavaScript to bytecode
const bytecode = quickjs.compileToByteCode("42;");
if (!bytecode || bytecode.length === 0)
  throw new Error("Failed to compile bytecode");

// Execute bytecode
const result = quickjs.evalByteCode(bytecode); // returns 42
if (result !== 42) throw new Error("Expected 42, got " + result);

Working with Modules

Compiling and Loading Modules

// Compile a module to bytecode
const bytecode = quickjs.compileToByteCode(
  `
  export function getNumber() {
    return 42;
  }
`,
  "math.js",
);

// Load the module
const mod = quickjs.loadByteCode(bytecode);
if (typeof mod !== "bigint")
  throw new Error("Expected module handle to be bigint");

// Call a function from the module
const result = quickjs.callModFunction(mod, "getNumber"); // returns 42
if (result !== 42) throw new Error("Expected 42, got " + result);

Working with Promises

Evaluating Async JavaScript

// Compile an async function
const bytecode = quickjs.compileToByteCode(
  `
  export async function test() {
    const result = await new Promise(resolve => resolve(883));
    return result;
  }
`,
  "test.js",
);

// Load the module
const mod = quickjs.loadByteCode(bytecode);

// Call the async function
const promise = quickjs.callModFunction(mod, "test");

// Get the result of the promise
const result = quickjs.getPromiseResult(promise); // returns 883
if (result !== 883) throw new Error("Expected 883, got " + result);

Interacting with Host Functions

Calling Host Functions from JavaScript

// Register a host function
quickjs.hostFunctions["sleep"] = async (params) => {
  const duration = quickjs.getObjectPropertyValue(params, "duration");
  await new Promise((resolve) => setTimeout(resolve, duration));
  return quickjs.allocateJSstring(`Slept for ${duration} ms`);
};

// Compile JavaScript that calls the host function
const bytecode = quickjs.compileToByteCode(
  `
  export async function test() {
    const result = await env.callHostAsync({ function_name: "sleep", duration: 500 });
    return result;
  }
`,
  "test.js",
);

// Load and call the function
const mod = quickjs.loadByteCode(bytecode);
const promise = quickjs.callModFunction(mod, "test");

// Wait for any pending async operations to complete
await quickjs.waitForPendingAsyncInvocations();

// Get the result
const result = quickjs.getPromiseResult(promise); // "Slept for 500 ms"
if (result !== "Slept for 500 ms")
  throw new Error("Expected 'Slept for 500 ms', got " + result);

The host-function contract

The pieces above fit together like this:

  1. The host registers functions in the hostFunctions registry: quickjs.hostFunctions["name"] = async (params) => { ... }.
  2. Guest code calls env.callHostAsync({ function_name: "name", ...params }) and awaits the result. function_name selects the entry in hostFunctions; the whole argument object is passed to the host function as an object handle — read values from it with getObjectPropertyValue(params, "propertyName").
  3. The host function returns a QuickJS value handle (e.g. from allocateJSstring), or null/undefined. Internally the wrapper resolves the guest's promise via the wasm export promise_callback(resolvingFunctions, result), which also runs QuickJS's pending-job loop so the guest continues past its await.
  4. Because host functions are async, the host must await quickjs.waitForPendingAsyncInvocations() before reading results with getPromiseResult(promise) — this drains all in-flight host invocations (including ones scheduled while draining).

Sandboxing untrusted code

Untrusted guest code can attempt to hang the host (while(true){}) or exhaust memory. Both can be bounded:

const quickjs = await createQuickJS();

// Cap how much memory the QuickJS runtime may allocate (bytes)
quickjs.setMemoryLimit(12 * 1024 * 1024);

// An allocation bomb now fails inside the sandbox instead of killing the host
try {
  quickjs.evalSource("new Array(1e9).fill(0);");
  throw new Error("expected the allocation to fail");
} catch (e) {
  if (!e.message.includes("out of memory")) throw e;
}

// The third parameter of evalSource is a timeout in milliseconds
try {
  quickjs.evalSource("while(true){}", "<evalsource>", 100);
  throw new Error("expected the eval to be interrupted");
} catch (e) {
  if (!e.message.includes("interrupted")) throw e;
}

// The sandbox is still fully usable afterwards
const result = quickjs.evalSource("42;");
if (result !== 42) throw new Error("Expected 42, got " + result);

callModFunction(mod, functionName, timeoutMs) and evalByteCode(bytecode, timeoutMs) accept the same timeout parameter. The timeout is enforced by a wall-clock check in QuickJS's interrupt handler, so it also covers pending jobs executed at the end of the call. It does not cover guest code resumed later by an async host-function response; requestInterrupt() can be called from a host function to terminate the guest when it next resumes.

When an eval fails — an exception thrown by guest code, an interrupted eval, or an exceeded memory limit — the wrapper throws an Error whose message is the QuickJS exception message.

The wasm linear memory is a fixed ~16.5MB and cannot grow, so that is the hard ceiling whatever limit you set. Running into it is still reported as a catchable out of memory exception and the instance stays usable, but keeping the limit below the ceiling makes the bound explicit and leaves room for the strings and buffers the host bindings allocate on the guest's behalf.

API Reference

TypeScript declarations ship with the package, so the distinction that matters most is visible at the call site: converted values (number | string | boolean | null | undefined) versus JSHandle, the opaque bigint reference to something still living inside the sandbox.

Core Functions

  • createQuickJS(): Creates a new QuickJS instance
  • evalSource(code, filename?, timeoutMs?): Evaluates JavaScript code
  • compileToByteCode(code, filename?): Compiles JavaScript code to bytecode
  • evalByteCode(bytecode, timeoutMs?): Executes bytecode
  • loadByteCode(bytecode): Loads a module from bytecode
  • callModFunction(module, functionName, timeoutMs?): Calls a function in a module (note: arguments are not currently supported)

Limits and interruption

  • setMemoryLimit(bytes): Caps QuickJS runtime allocations; exceeding the cap throws an "out of memory" exception inside the sandbox
  • timeoutMs parameters: interrupt the guest with an "interrupted" exception once the wall-clock deadline passes
  • requestInterrupt(): Requests that the guest is interrupted at its next resumption (useful from async host functions); cleared when the next timeout-guarded call completes

Promise Handling

  • getPromiseResult(promise): Gets the result of a settled promise
  • waitForPendingAsyncInvocations(): Waits for all pending async operations to complete

Object Manipulation

  • getObjectPropertyValue(object, propertyName): Gets a property value from a JavaScript object
  • allocateJSstring(string): Creates a JavaScript string in the QuickJS environment

There is one escape hatch, which one-shot users never need: freeValue(handle) releases an object handle early. It exists only for a single run that produces handles in the hundreds of thousands, where they would otherwise fill the instance's heap before the run finishes. If you are wondering whether you should be calling it, you should not.

Host Function Integration

  • hostFunctions: Object to register functions that can be called from JavaScript via env.callHostAsync() (see The host-function contract)

Value conversion

Return values from the sandbox are converted to host values: integers, floats, booleans, strings (UTF-8 safe), null and undefined map to their host equivalents; objects (including promises and module handles) are returned as opaque bigint handles for use with getObjectPropertyValue/getPromiseResult/callModFunction.

Converted values are released as they cross the boundary; handles are reclaimed when the instance is dropped. Neither requires anything from you.

A failed compile throws rather than returning empty bytecode, so a source too large or too broken to compile is reported at compileToByteCode instead of surfacing later as a call on a broken module.

Building from source

./build.sh produces jseval.wasm. It downloads a pinned QuickJS release (2026-06-04) and uses emcc from the PATH, bootstrapping a pinned emsdk if none is installed. The build is reproducible: two clean builds with the same toolchain produce byte-identical wasm.

Examples

Basic Example

const quickjs = await createQuickJS();
const result = quickjs.evalSource("40 + 2"); // returns 42
if (result !== 42) throw new Error("Expected 42, got " + result);

Module Example

const quickjs = await createQuickJS();
const bytecode = quickjs.compileToByteCode(
  `
  export function greet() {
    return "Hello, World!";
  }
`,
  "greet.js",
);
const mod = quickjs.loadByteCode(bytecode);
const greeting = quickjs.callModFunction(mod, "greet"); // returns "Hello, World!"
if (greeting !== "Hello, World!")
  throw new Error("Expected 'Hello, World!', got " + greeting);

Async Example with Host Functions

const quickjs = await createQuickJS();

// Register a host function
quickjs.hostFunctions["fetchData"] = async (params) => {
  const url = quickjs.getObjectPropertyValue(params, "url");
  const response = await fetch(url);
  const data = await response.json();
  return quickjs.allocateJSstring(JSON.stringify(data));
};

// Create and run code that uses the host function
const bytecode = quickjs.compileToByteCode(
  `
  export async function getData() {
    const data = await env.callHostAsync({
      function_name: "fetchData",
      url: "https://api.example.com/data"
    });
    return data; // Return as string, objects become bigint references
  }
`,
  "fetch.js",
);

const mod = quickjs.loadByteCode(bytecode);
const promise = quickjs.callModFunction(mod, "getData");
await quickjs.waitForPendingAsyncInvocations();
const result = quickjs.getPromiseResult(promise); // Returns JSON string
const parsedResult = JSON.parse(result); // Parse on the host side
if (!parsedResult.status || parsedResult.status !== "success") {
  throw new Error("Expected success status in result");
}