Preview2 Shim
WASI Preview2 implementations for Node.js & browsers.
Node.js support is fully tested and conformant against the Wasmtime test suite.
Browser support is available with the platform limitations documented below.
The Node.js implementation owns its worker artifact. Direct package use and supported downstream
bundlers should resolve it through the public shim imports; applications do not need to import or
copy files from dist/io.
Browser support matrix
Browser defaults are capability-safe: clocks and secure randomness use Web APIs, stdout and stderr
write to the console, stdin is closed, outbound HTTP uses fetch, filesystem preopens must be
configured explicitly, and raw sockets are unavailable unless an embedding supplies an adapter.
| WASI area | Browser status | Default capability |
|---|---|---|
| CLI environment and arguments | Configurable per WASIShim; compatibility setters are global |
Empty snapshots unless configured |
| CLI stdin | Adapter-backed | Closed stream |
| CLI stdout and stderr | Web API | Console-backed, preserving split UTF-8 writes until flush/newline |
| CLI terminals | Adapter-backed | No terminal resource |
| Clocks | Web API | performance.now, Date.now, and timer-backed pollables |
| Random | Web API | crypto.getRandomValues, including requests larger than 64 KiB |
| I/O streams and poll | Implemented browser resources | Non-blocking streams depend on their injected handlers |
| Filesystem | Adapter-backed; opt-in in-memory compatibility implementation | No persistent storage is selected implicitly |
| Outbound HTTP | Web API | Delegates to fetch |
| Incoming HTTP | Adapter-backed; opt-in in-memory client | Browsers cannot listen for arbitrary inbound HTTP |
| TCP and UDP | Adapter-backed; opt-in in-memory implementations | Raw sockets are not exposed by standard browsers |
| DNS | Host adapter required | DNS is not exposed independently by standard browsers |
WASIShim instantiation |
Implemented | Interface namespaces can be overridden per instance |
An operation is not considered supported merely because its interface shape exists. Adapter-backed rows require the embedding application to provide that capability; unavailable operations fail with a WASI-domain error instead of logging or returning a placeholder resource.
Detailed browser capabilities
The following table describes the built-in browser implementation. An application-provided
namespace can replace any row through WASIShim.
| Interface | Implemented | Host adapter required | Unsupported by browser implementation |
|---|---|---|---|
wasi:cli |
environment, arguments, initial cwd, exit, stream and terminal accessors | stdin/stdout/stderr handlers and terminal resources | — |
wasi:clocks |
wall clock, monotonic clock, timer subscriptions | — | timezone APIs (not part of Preview 2) |
wasi:random |
secure and insecure bytes, insecure seed | — | — |
wasi:io |
errors, input/output streams, poll and pollables | readiness and I/O behavior for injected stream handlers | synchronous blocking of the browser event loop |
wasi:filesystem |
descriptors, files, directories, links, metadata, streams, preopens through the ephemeral adapter | persistent storage, permissions, and external file handles | symbolic-link creation and reading |
wasi:http/outgoing-handler |
Fetch-backed requests and buffered request bodies | Fetch implementation and network permission | request/response trailers; streaming uploads |
wasi:http/incoming-handler |
request/response translation, injectable handler, and in-memory client | HTTP server, service worker, or other request source | direct browser listening |
wasi:sockets/ip-name-lookup |
interface shape only | complete interface replacement | built-in DNS lookup |
wasi:sockets/tcp* |
opt-in in-memory server/client implementation | adapter for external connectivity | built-in raw TCP |
wasi:sockets/udp* |
opt-in in-memory server/client implementation | adapter for external connectivity | built-in raw UDP |
Outbound HTTP buffers a requested body until outgoing-body.finish before calling fetch.
This preserves complete-body semantics across browsers but does not provide streaming upload or
upload backpressure. Incoming Fetch bodies retain their asynchronous stream behavior. HTTP
trailers are not implemented.
Chromium-based browsers can opt into Fetch request streaming. This setting uses a
ReadableStream request body with duplex: "half"; unsupported browsers reject the request, so
applications should enable it only after applying their own browser support policy or feature
detection:
import { http } from "@bytecodealliance/preview2-shim";
http._setRequestStreaming(true);
The setting affects subsequent requests made through the browser HTTP shim. Call
http._setRequestStreaming(false) to restore portable completion buffering.
Host-side helper functions that build on the public shim surface are exported from helpers/
subpaths, separate from the underscore-prefixed configuration setters. The writeFileAt and
readFileAt helpers (@bytecodealliance/preview2-shim/helpers/filesystem) move bytes between host
JavaScript and a preopened directory without a guest, on both builds. The browser-only
bufferedInputStream helper (@bytecodealliance/preview2-shim/browser/helpers/cli) feeds a guest a
fixed stdin buffer. See the filesystem helpers and CLI helpers
pages in the Jco book.
Browser applications select storage explicitly. The bundled file-data adapter is ephemeral and must be opted into:
import { filesystem } from "@bytecodealliance/preview2-shim";
import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";
const shim = new WASIShim({
environment: { MODE: "browser" },
arguments: ["component"],
stdout: { write: (bytes) => terminal.write(bytes) },
browserFilesystem: {
adapter: new filesystem.InMemoryFilesystemAdapter(),
preopens: { "/data": { dir: {} } },
},
sandbox: { enableNetwork: false },
});
The browser shim does not request File System Access permissions or choose IndexedDB/OPFS on an
application's behalf. Applications that need another storage model implement the generated
wasi:filesystem/types and wasi:filesystem/preopens namespaces and inject them through the
filesystem option:
const shim = new WASIShim({
filesystem: {
types: applicationFilesystemTypes,
preopens: applicationFilesystemPreopens,
},
});
This keeps permission prompts, handle acquisition, persistence, and synchronization policy in
application code. sandbox.enableNetwork: false denies raw TCP, UDP, DNS, and outbound HTTP.
Deterministic in-memory transports are available for components that act as servers and for tests:
import { http, sockets } from "@bytecodealliance/preview2-shim";
const tcpSockets = new sockets.InMemoryTcpSockets();
const tcpClient = tcpSockets.connect(serverAddress);
const udpSockets = new sockets.InMemoryUdpSockets();
const udpClient = udpSockets.createClient(clientAddress);
const httpClient = new http.InMemoryHttpClient(component.incomingHandler);
const shim = new WASIShim({ tcpSockets, udpSockets });
These implementations route bytes only within the current JavaScript realm; they do not grant raw
browser network access. InMemoryHttpClient.fetch(request) returns a standard Web Response.
For a small application-owned implementation, see the
Map-backed browser filesystem test shim. It keeps named
roots in an in-memory Map, implements createPreopens, and is intentionally example code rather
than a published or supported filesystem package. The example is exercised through the reusable
filesystem implementation test suite, which can also be pointed
at other implementations.
Browser filesystem adapters own the capabilities passed in preopens and the roots returned from
getRoot. A root may be shared by multiple descriptors and preopen names; the adapter is therefore
responsible for persistence and synchronization of shared mutations. Calling dispose on the
namespace returned by createFilesystem calls the adapter's optional dispose method once and
invalidates further preopen access. WASIShim does not currently cascade disposal, so embeddings
using external handles must retain and dispose their application-owned filesystem namespace or
adapter themselves. The bundled in-memory adapter keeps all state in memory and shares mutations
for the same file-data object.
Features
WASI Shim object for easy instantiation
An default instantiation object can be used via the WASIShim class in @bytecodealliance/preview2-shim/instantiation:
import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";
import type {
VersionedWASIImportObject,
WASIImportObject,
} from "@bytecodealliance/preview2-shim/instantiation";
const shim = new WASIShim();
const unversioned: WASIImportObject = shim.getImportObject();
// console.log('unversioned', unversioned);
unversioned satisfies WASIImportObject;
unversioned satisfies VersionedWASIImportObject<"">;
const versioned: VersionedWASIImportObject<"0.2.3"> = shim.getImportObject({
asVersion: "0.2.3",
});
//console.log('versioned', versioned);
versioned satisfies VersionedWASIImportObject<"0.2.3">;
The import object generated by getImportObject can be easily used in instantiate() calls
produced by jco transpile (with --instantiation=async):
import { WASIShim } from '@bytecodealliance/preview2-shim/instantiation';
// The code below assumes that you have output your transpiled WebAssembly component to `dist/transpiled`
import { instantiate } from './dist/transpiled/component.js';
const loader = async (path: string) => {
const buf = await readFile(`./dist/transpiled/${path}`);
return await WebAssembly.compile(buf.buffer as ArrayBuffer);
};
const component = await instantiate(loader, new WASIShim().getImportObject());
// TODO: Code that uses your component's exports goes here.
Sandboxing
On Node.js, the preview2-shim provides host filesystem, environment, and network access by default, matching the usual behavior of Node.js libraries. Browser defaults expose no filesystem preopens or raw sockets. Both platforms can configure which capabilities a guest receives.
Using WASIShim for sandboxing
The WASIShim class accepts a sandbox configuration option to control access:
import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";
// Fully sandboxed - no filesystem, network, or env access
const sandboxedShim = new WASIShim({
sandbox: {
preopens: {}, // No filesystem access
env: {}, // No environment variables
args: ["arg1"], // Custom arguments
enableNetwork: false, // Disable network access
},
});
// Node.js only: map virtual paths to host paths
const limitedShim = new WASIShim({
sandbox: {
preopens: {
"/data": "/tmp/guest-data", // Guest sees /data, maps to /tmp/guest-data
"/config": "/etc/app", // Guest sees /config, maps to /etc/app
},
env: { ENV1: "42" }, // Only expose specific env vars
},
});
const component = await instantiate(loader, sandboxedShim.getImportObject());
Notes on sandboxing
Directory mutation compatibility (Node.js)
Node filesystems preserve the existing behavior by default: mutate-directory
requests fail with unsupported, and omitting that flag on a child directory
does not make it read-only. Some guests, including adapted Preview 1 components,
depend on that behavior. See the earlier implementation discussion.
Opt in to mutate-directory support for guests that use its permission semantics:
import { filesystem } from "@bytecodealliance/preview2-shim";
import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation";
const guestFilesystem = filesystem.createFilesystem({
preopens: { "/data": "/tmp/guest-data" },
enableMutateDirectory: true,
});
const shim = new WASIShim({ filesystem: guestFilesystem });
With this option, preopens have read and mutate-directory capabilities,
subject to host permissions. A child directory opened without
mutateDirectory: true rejects mutations, metadata changes, and writable,
create, or truncate opens with read-only. Request the flag for writable
directory handles. Returned flags are snapshots and cannot change permissions.
Some WASI guests, including Rust-based runtimes, also request mutate-directory
when opening a regular file. When enabled, the shim accepts and clears that
directory-only flag on files. This grants no additional authority: the original
request passes the parent permission check, file writes still require write,
and directories retain the flag for subsequent mutation checks. Wasmtime also
accepts the extra file flag, but its broader permission semantics are still
under discussion.
For components using the global filesystem imports, opt in with
filesystem._setPreopens({ "/data": "/tmp/guest-data" }, { enableMutateDirectory: true }).
_addPreopen accepts the same options as its third argument. The option is
captured by each new preopen and inherited by its descendants; it does not
change existing handles or other filesystem instances.
These rules apply to the Node filesystem implementation. Browser adapters and application-provided filesystem implementations define their own behavior.
Default access
By default (when no options are passed), the shim is providing full access to match typical Node.js library behavior. In browsers, filesystem preopens remain empty until the application explicitly injects filesystem namespaces or selects the ephemeral file-data adapter.
Preopen mappings
sandbox.preopens maps guest paths to Node.js host paths on Node.js. With
browserFilesystem, the same option maps guest paths to capabilities understood by its adapter
and overrides browserFilesystem.preopens. A custom filesystem can implement
createPreopens(preopens) to interpret the provided properties and return its own
wasi:filesystem/preopens namespace. The shim passes those properties through unchanged.
Instance isolation and global configuration
Each WASIShim instance has its own isolated preopens, environment variables, and arguments.
Multiple instances with different configurations will not affect each other.
The direct preopen functions (_setPreopens, _clearPreopens, etc.) modify global state and
affect all components not using WASIShim with explicit configuration. For isolation, prefer
using WASIShim with the sandbox option containing preopens and env.
Network access
When sandbox.enableNetwork: false, socket operations and outbound HTTP receive instance-local
denied network capabilities.
License
This project is licensed under the Apache 2.0 license with the LLVM exception. See LICENSE for more details.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.
Host IO extensions
Opt-in providers can use the Node-only @bytecodealliance/preview2-shim/io-worker
entry point to operate on existing streams in the shim's IO worker. Host-selected
modules load lazily and share the worker's stream, future, and poll ownership rules.
Guest code cannot select extension modules.
The Node wasi:tls provider and its TLS policy live in
jco-std, which wraps the supplied TCP streams
without opening a replacement connection.