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

@passivelogic/uwasi

v1.6.2

Published

Micro modularized WASI runtime for JavaScript

Readme

npm version .github/workflows/test.yml

μWASI

This library provides a WASI implementation for Node.js and browsers in a tree-shaking friendly way. The system calls provided by this library are configurable.

With minimal configuration, it provides WASI system calls which just return WASI_ENOSYS.

Features

Installation

npm install uwasi

Example

With all system calls enabled

import { WASI, useAll } from "uwasi";
import fs from "node:fs/promises";

async function main() {
    const wasi = new WASI({
        args: process.argv.slice(2),
        features: [useAll()],
    });
    const bytes = await fs.readFile(process.argv[2]);
    const { instance } = await WebAssembly.instantiate(bytes, {
        wasi_snapshot_preview1: wasi.wasiImport,
    });
    const exitCode = wasi.start(instance);
    console.log("exit code:", exitCode);

/* With Reactor model
    wasi.initialize(instance);
*/
}

main()

With no system calls enabled

import { WASI, useAll } from "uwasi";

const wasi = new WASI({
    features: [],
});

With environ, args, clock, proc, and random enabled

import { WASI, useArgs, useClock } from "uwasi";

const wasi = new WASI({
    args: ["./a.out", "hello", "world"],
    features: [useEnviron(), useArgs(), useClock(), useProc(), useRandom()],
});

With fd (file descriptor) enabled only for stdio

By default, stdin behaves like /dev/null, stdout and stderr print to the console.

import { WASI, useStdio } from "uwasi";

const wasi = new WASI({
    features: [useStdio()],
});

You can use custom backends for stdio by passing handlers to useStdio.

import { WASI, useStdio } from "uwasi";

const inputs = ["Y", "N", "Y", "Y"];
const wasi = new WASI({
    features: [useStdio({
        stdin: () => inputs.shift() || "",
        stdout: (str) => document.body.innerHTML += str,
        stderr: (str) => document.body.innerHTML += str,
    })],
});

By default, the stdout and stderr handlers are passed strings. You can pass outputBuffers: true to get Uint8Array buffers instead. Along with that, you can also pass Uint8Array buffers to stdin.

import { WASI, useStdio } from "uwasi";
const wasi = new WASI({
    features: [useStdio({
        outputBuffers: true,
        stdin: () => new Uint8Array([1, 2, 3, 4, 5]),
        stdout: (buf) => console.log(buf),
        stderr: (buf) => console.error(buf),
    })],
});

To give the guest more character devices than stdio, pass extraFds, keyed by fd number. Each entry has either a read handler (like stdin) or a write handler (like stdout). The guest can't discover these fds, so it has to be told which numbers to use.

import { WASI, useStdio } from "uwasi";
const wasi = new WASI({
    features: [useStdio({
        extraFds: {
            3: { write: (lines) => report.push(lines) },
            4: { read: () => "config\n" },
        },
    })],
});

useMemoryFS accepts the same option through withStdio. There, the preopened directories take fd 3 upward, because wasi-libc stops looking for preopens at the first fd that isn't one. Extra fds must be numbered above them, e.g. from 4 with the default single / preopen; a clash throws a RangeError.

With poll_oneoff and sched_yield enabled

usePoll supplies the blocking primitives that libc sleep functions (nanosleep, usleep, timed waits) are built on. Clock subscriptions block the calling thread until the earliest deadline using Atomics.wait where the host allows it, falling back to a busy-wait (e.g. on the browser main thread). Since all file descriptors in this runtime are synchronous, fd_read/fd_write subscriptions report ready immediately.

import { WASI, useStdio, usePoll } from "uwasi";

const wasi = new WASI({
    features: [useStdio(), usePoll()],
});

The blocking strategy is replaceable, e.g. to integrate with a host scheduler:

const wasi = new WASI({
    features: [usePoll({ sleep: (ms) => mySynchronousSleep(ms) })],
});

usePoll is included in useAll().

Genuine stdin readiness with SharedInputChannel

For readiness-driven guests (e.g. poll(2)-based event loops or libdispatch fd sources), SharedInputChannel connects a producing thread — a worker pumping a pipe, or a UI thread collecting keystrokes — to the guest thread over a SharedArrayBuffer ring. poll_oneoff then genuinely parks the guest (Atomics.wait) until input arrives, end of file, or a clock deadline, and reads drain the buffer without blocking. Producer close is delivered as an fd hangup event (POLLHUP through libc poll).

// Guest thread
import { WASI, useStdio, usePoll, SharedInputChannel } from "uwasi";
const channel = new SharedInputChannel();
// hand channel.sharedBuffer to the producing thread...
const wasi = new WASI({
    features: [
        useStdio({ stdin: channel.stdin() }),
        usePoll({ fdReadiness: channel.fdReadiness() }),
    ],
});

// Producing thread (worker or main thread)
const producer = new SharedInputChannel(sharedBufferFromGuestThread);
producer.push(new TextEncoder().encode("hello"));
producer.close(); // end of file

Atomics.wait is unavailable on a browser main thread, so run the guest in a worker there; waits degrade to a busy-wait otherwise. In browsers, SharedArrayBuffer additionally requires the page to be cross-origin isolated (Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp response headers). Node.js and worker threads need no special setup.

Durable files over OPFS with useOPFS

useOPFS provides the same filesystem surface as useMemoryFS, backed by the Origin Private File System so files survive page reloads and worker restarts. It runs over OPFS sync access handles, which browsers only expose in workers, so instantiate it in a worker. The backend needs asynchronous setup (acquiring access handles), so it is constructed up front and handed to the feature:

import { WASI } from "uwasi";
import { useOPFS, OPFSBackend } from "uwasi/opfs";

const backend = await OPFSBackend.create(
    await navigator.storage.getDirectory(),
);
const wasi = new WASI({
    features: [useOPFS({ withBackend: backend })],
});
// ... run the guest ...
await backend.close(); // graceful shutdown; a dying worker is also safe

The directory handed to OPFSBackend.create becomes a mapped store owned by uwasi (content files with opaque names plus a checksummed namespace record), not a 1:1 mirror of the guest tree; unrelated files already in the directory are ignored and untouched. Namespace changes (create, unlink, rename) are durable when the syscall returns, and fd_sync/fd_datasync really flush — ordered so that sidecar-file lifecycles survive a crashed worker: a rollback journal unlinked at commit can never resurrect into the next worker, and one that was not unlinked survives byte-for-byte.

Files can be seeded through backend.fileSystem (a MemoryFileSystem) before the guest starts; call await backend.persistAll() afterwards to push them to storage. Creating files is synchronous thanks to a pool of pre-created spares (spareFiles option, default 16); a guest call that claims more spares than that stays correct but defers content durability until the event loop turns (fd_sync fails honestly with NOSPC until then). Size spareFiles to the most spares one guest call claims: one per file it creates and per host-seeded file it opens first, plus seeded files a snapshot of the namespace takes along and spares that failed creates leave waiting for the next snapshot. await backend.settle() between guest calls to give every file its data file (so fd_sync works) and refill the pool; see Sizing the OPFS Spare Pool. Hard links return NOTSUP, as do linking, renaming, replacing and unlinking device nodes such as /dev/null, which the runtime recreates at every open.

The store format is not compatible with stores written by earlier builds of this OPFS backend, in its JSON format or an earlier binary one. OPFSBackend.create refuses such a store rather than open it, and leaves it untouched, as it does any store whose format it cannot read, such as one a later version wrote.

Custom storage backends

uwasi/filesystem exposes the synchronous FSBackend contract, namespace types, storage errno constants, and useFileSystem provider. It keeps WASI path resolution, descriptors, rights checks, and guest-memory handling out of storage backends. uwasi/opfs exposes the existing OPFS implementation. OPFS is available only through the uwasi/opfs public subpath. Root imports of WASI and useAll do not load OPFS code in either ESM or CommonJS; useAll() continues to select memory storage. Existing fork consumers must move OPFSBackend and useOPFS imports from uwasi to uwasi/opfs, keeping WASI imported from uwasi as shown above. Non-OPFS root exports are unchanged.

Everything remains in one npm package. The OPFS source is isolated so it can later move into a separate package without copying syscall handling. uwasi/opfs is a subpath of this same package, not a separate dependency. A future extraction into another package would require consumers to change their OPFS import specifier and install that package. See Filesystem Backends for the public boundary, source layout, extraction test, and unchanged storage limits.

Implementation Status

43 of the 46 WASI preview1 functions are implemented (the three socket-transfer calls are deliberately absent — preview1 sockets are vestigial and were replaced wholesale in preview2). The filesystem surface is provided by useMemoryFS (in-memory) and useOPFS (durable, browser workers), both validated against the full wasi-testsuite with zero skipped cases using the in-memory backend and an OPFS mock, not a real browser (useOPFS differs only in refusing hard links and changes to device nodes with NOTSUP); useStdio provides the stdio subset only.

| Syscall | Status | Notes | |-------|----------|---------| | args_get / args_sizes_get | ✅ | | | clock_res_get / clock_time_get | ✅ | CPU-time clocks are approximated by the monotonic clock | | environ_get / environ_sizes_get | ✅ | | | fd_advise | ✅ | Validates the advice; otherwise a no-op | | fd_allocate | ✅ | Grows the file to offset + len, never shrinks | | fd_close | ✅ | Preopens are closable | | fd_datasync / fd_sync | ✅ | Memory FS: no-op success (memory is always "synced"); OPFS: a real flush() of the file's sync access handle; on a directory, success at once, since every guest namespace change is durable when its syscall returns; it does not record host-seeded files (see persistAll()) | | fd_fdstat_get | ✅ | Reports real per-fd flags and rights | | fd_fdstat_set_flags | ✅ | APPEND honored by fd_write | | fd_fdstat_set_rights | ✅ | Rights may only shrink (NOTCAPABLE otherwise) | | fd_filestat_get | ✅ | Stable per-node inodes, real sizes and timestamps | | fd_filestat_set_size | ✅ | Zero-fills growth | | fd_filestat_set_times | ✅ | Validates fstflags combinations | | fd_pread / fd_pwrite | ✅ | Positional; never move the cursor; pwrite ignores APPEND | | fd_prestat_get / fd_prestat_dir_name | ✅ | | | fd_read / fd_write | ✅ | Rights-checked; APPEND writes at end of file | | fd_readdir | ✅ | Cookie-paged with ./.. entries and real inodes | | fd_renumber | ✅ | Destination must be open; source is closed | | fd_seek / fd_tell | ✅ | ISDIR on directories, SPIPE on character devices, INVAL on negative seek | | path_create_directory | ✅ | Single level; parent must exist | | path_filestat_get | ✅ | SYMLINK_FOLLOW honored | | path_filestat_set_times | ✅ | Symlink-aware (lstat-level timestamps) | | path_link | ✅ | Memory FS: hard links with shared inode and nlink accounting; OPFS: NOTSUP for files and symlinks (one name per node in the durable namespace record) | | path_open | ✅ | Full oflags/fdflags/rights semantics; sandboxed path resolution | | path_readlink | ✅ | Silent truncation to the buffer, no NUL | | path_remove_directory | ✅ | NOTEMPTY on non-empty directories | | path_rename | ✅ | POSIX replace semantics incl. empty-directory targets | | path_symlink | ✅ | Relative targets only; dangling links allowed | | path_unlink_file | ✅ | Removes symlinks without following | | poll_oneoff | ✅ | Clock subscriptions block the thread (Atomics.wait, busy-wait fallback); fd subscriptions report ready immediately by default, or genuine readiness via SharedInputChannel/fdReadiness | | proc_exit | ✅ | | | proc_raise | ✅ | Exits with 128 + signal | | random_get | ✅ | | | sched_yield | ✅ | No-op success on a single-threaded host | | sock_shutdown | ✅ | Error reporting only (BADF / NOTSOCK) | | sock_accept / sock_recv / sock_send | ❌ | Deliberately absent; superseded by preview2 wasi:sockets |

Path resolution is sandboxed per directory fd: ./../// normalize, .. cannot escape the fd, absolute paths and absolute symlink targets are rejected (PERM), intermediate symlinks always expand, and the final symlink expands only with LOOKUPFLAGS_SYMLINK_FOLLOW (loop budget 32, then LOOP).

Spec conformance notes

uwasi targets WASI preview1. Four behaviors deliberately go beyond or beside the letter of the preview1 spec; all are defaults chosen for compatibility on single-threaded JavaScript hosts, and all guest-visible surface remains the plain wasi_snapshot_preview1 namespace:

  • CPU-time clocks (clockid 2/3) are answered with the monotonic clock. Preview2 dropped these clocks as impractical to implement, and wasi-clocks documents wasi-libc's strategy of emulating them with the monotonic clock — uwasi applies the same sanctioned emulation at the host. (wasmtime instead rejects these clock IDs.)
  • Without a readiness provider, poll_oneoff fd subscriptions report ready immediately with nominal nbytes (1 for reads, 65536 for writes) rather than actual availability. Wire usePoll({ fdReadiness }) (e.g. via SharedInputChannel) for genuine readiness. Preview2 removed byte counts from poll results entirely; preview3 removed readiness polling.
  • poll_oneoff returns ENOTSUP for waits that can never complete (not-ready fds with no way to wait and no clock deadline) instead of blocking forever on the only thread. Preview1 does not define this failure mode; preview3's completion-based async dissolves the problem.
  • proc_raise terminates with exit code 128 + signal for every signal. There is no signal machinery to deliver to; modern wasi-libc no longer calls proc_raise, and preview2/preview3 removed signals.

Host-side APIs beyond the preview1 surface (usePoll's sleep/ fdReadiness options, SharedInputChannel) are embedder configuration, invisible to guests. They intentionally mirror preview2 shapes — a WASIFdReadiness is a pollable, a SharedInputChannel is an input-stream producer — so a future preview2 host layer can reuse them.

Releasing

Run Actions > Release > Run workflow from main with a new stable version, such as 1.5.0 (no v prefix). CI updates and tests both manifests, commits the version bump, pushes the commit and tag, then publishes to npm. Existing tags are rejected. If publishing fails after the tag is pushed, publish from that tag separately rather than rerunning release creation.