Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

node:vfs

Jco implements the experimental Node 26.8.2 VFS API. Application code keeps its ordinary node:vfs imports. create() uses an isolated MemoryProvider by default; RealFSProvider accesses an explicitly supplied filesystem capability. Only the node: specifier is intercepted.

This provider selection mirrors Node.js’s node:vfs API. MemoryProvider and RealFSProvider are upstream Node.js classes: application code calls create() for memory storage, or explicitly passes new RealFSProvider(root) for filesystem storage. Jco’s --with-nodejs-vfs-via option selects the backend used by RealFSProvider; it does not change the in-memory default of create().

import { create, RealFSProvider } from 'node:vfs';

export function run(root) {
    const fs = create(new RealFSProvider(root));
    fs.mkdirSync('/reports', { recursive: true });
    fs.writeFileSync('/reports/result.txt', 'done');
    return fs.readFileSync('/reports/result.txt', 'utf8');
}

Omit the provider to use memory. Memory operations never consult host providers, preopens, or a storage resolver. MemoryProvider.setReadOnly() prevents subsequent write operations through the provider while preserving existing contents.

Choosing a filesystem implementation

SelectionHost capabilityDefault behavior
--with-nodejs-vfs-via directjco:node/fs@0.1.0Filesystem access is denied with ERR_JCO_FS_ADAPTER_REQUIRED.
direct with an explicit Node host mappingjco:node/fs@0.1.0Node filesystem passthrough under each RealFSProvider root.
--with-nodejs-vfs-via wasi-filesystemwasi:filesystem/preopens and types at 0.2.12Access is limited to the preopens supplied at instantiation.

direct is the default. It reuses the same filesystem boundary and host provider as node:fs; mapping that capability grants it to both APIs in the component. Imports and provider construction do not themselves perform filesystem operations. The selected host is accessed lazily when an operation needs it.

For Node passthrough:

jco componentize app.js --wit wit --bundle -o app.wasm
jco transpile app.wasm -o out \
  --map 'jco:node/fs@0.1.0=@bytecodealliance/jco-std/wasi/0.2.x/node/26.x.x/vfs/host/node'

The vfs/host/node export reuses the existing Node filesystem provider. It does not depend on the host having native node:vfs or enabling --experimental-vfs. The VFS façade, virtual descriptors, and memory tree run inside the component.

For WASI filesystem access:

jco componentize app.js --wit wit --bundle \
  --with-nodejs-vfs-via wasi-filesystem -o app.wasm
jco transpile app.wasm -o out --instantiation async

Jco adds the selected interfaces and their WIT dependencies. Configure preopens when instantiating the result, for example with preview2-shim:

import { instantiate } from './out/app.js';
import { WASIShim } from '@bytecodealliance/preview2-shim/instantiation';

const wasi = new WASIShim({
    sandbox: { preopens: { '/data': '/srv/application-data' } },
});
const app = await instantiate(undefined, wasi.getImportObject());
app.run('/data');

VFS roots and application paths use POSIX syntax. Real provider roots must be absolute. Filesystem root directories must already exist. Relative paths are resolved from the VFS root, not the host process’s working directory. Root and symlink checks are compatibility checks; VFS is not a replacement for host isolation or correctly scoped WASI capabilities.

Configuring storage placement

By default, a WASI real provider selects the longest preopen mount containing its root. For a preopen /data, new RealFSProvider('/data/projects/demo') stores contents in projects/demo within that preopen. A root with no matching preopen fails with EACCES. An exact mount match uses the preopen’s root directory.

Supply a guest JavaScript module exporting resolveRoot to select another preopen or directory:

// vfs-storage.js
export function resolveRoot(rootPath, preopens) {
    const storage = preopens.find(([, name]) => name === '/data');
    if (!storage || rootPath !== '/workspace') {
        throw new Error('No storage configured for this VFS root');
    }
    return { descriptor: storage[0], directory: 'projects/demo' };
}
jco componentize app.js --wit wit --bundle \
  --with-nodejs-vfs-via wasi-filesystem \
  --with-nodejs-vfs-wasi-config ./vfs-storage.js -o app.wasm

The module path is resolved from the command’s working directory and bundled into the guest. The callback receives the normalized VFS root and [descriptor, guestPath] preopen pairs. It returns a borrowed descriptor and a directory relative to it. Absolute directories and .. paths escaping the preopen are rejected. The selected directory must already exist. The resolver runs once per real provider, on first use; separate VFS instances can choose different folders. Exceptions propagate to the caller. The implementation disposes descriptors it opens, but never disposes the resolver’s borrowed preopen.

Direct adapter users can configure the same callback through createWasiVfs({ preopens, resolveRoot }) from @bytecodealliance/jco-std/wasi/0.2.x/node/26.x.x/vfs/impl/wasi-filesystem. Direct adapters and native Node builtins can coexist in one host application. Application components should continue importing node:vfs.

Supported operations and limits

The module exports create, VirtualFileSystem, VirtualProvider, MemoryProvider, and RealFSProvider. File contents, directories, copy/rename, hard links, symbolic links, metadata, directory handles, and scalar virtual file-descriptor I/O are supported. Callback and promise façades share the same provider. Custom providers inherit the base class’s derived file operations. VFS openAsBlob() returns a snapshot when the component engine supplies Blob. As in the pinned runtime, vfs.promises.open() returns a numeric virtual file descriptor, not a Node fs.promises.FileHandle.

Mounting into native node:fs or the module loader, filesystem streams, and watchers throw ERR_JCO_UNSUPPORTED_NODE_API. mounted stays false and mountPoint stays null. Providers report supportsWatch: false. Missing custom provider primitives throw ERR_METHOD_NOT_IMPLEMENTED. The component does not emit Node’s process-wide experimental warning. Memory metadata uses UID/GID zero rather than consulting the host process.

WASI 0.2.12 has no chmod/chown or access-permission test operation. Those calls fail explicitly; existence-only access works. WASI stat fields absent from the interface use zero for device, inode, ownership and birth time, and conventional file/directory mode bits. Actual size, link count and available timestamps come from WASI. Directory listing order is host-dependent. Resource operations use WASI’s synchronous descriptor methods and preserve 64-bit offsets.

Provenance

The portable VFS algorithms are adapted from Node v26.8.2, commit f2f2c2f246c36bd74f082cb43ecfe830657d81c9, with MIT attribution retained. The implementation reuses Jco’s portable filesystem types, value objects, validation, error transport, and Node host provider. Audited unenv 2.0.0-rc.24 has no VFS implementation. Its alias map is not enabled for this module.