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.
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. Raw TCP, UDP, and DNS are denied by default; outbound HTTP remains a separate
fetch capability.
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
- 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.
sandbox.preopensmaps guest paths to Node.js host paths on Node.js. WithbrowserFilesystem, the same option maps guest paths to capabilities understood by its adapter and overridesbrowserFilesystem.preopens. A customfilesystemcan implementcreatePreopens(preopens)to interpret the provided properties and return its ownwasi:filesystem/preopensnamespace. The shim passes those properties through unchanged.- Each
WASIShiminstance 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 usingWASIShimwith explicit configuration. For isolation, prefer usingWASIShimwith thesandboxoption containingpreopensandenv. - When
sandbox.enableNetwork: false, Node.js socket operations receive an instance-local denied network capability. Outbound HTTP is a separate Fetch capability; replace or omit the HTTP namespace when the embedding must deny it as well.
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.