Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
- [`node:tty`](./interop/nodejs-builtins/supported-modules/tty.md)
- [`node:url`](./interop/nodejs-builtins/supported-modules/url.md)
- [`node:util`](./interop/nodejs-builtins/supported-modules/util.md)
- [`node:vfs`](./interop/nodejs-builtins/supported-modules/vfs.md)
- [`node:vm`](./interop/nodejs-builtins/supported-modules/vm.md)
- [`node:wasi`](./interop/nodejs-builtins/supported-modules/wasi.md)
- [`node:worker_threads`](./interop/nodejs-builtins/supported-modules/worker-threads.md)
Expand Down
3 changes: 2 additions & 1 deletion docs/src/interop/nodejs-builtins/supported-modules/fs.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ When either specifier occurs in bundled source, Jco adds a missing import to the
selected world, installs `fs.wit` under `deps/jco-node-0.1.0`, and prints a CLI
warning to alert to the fact that a WIT dependency has been added.

The default filesystem host provider always throws `ERR_JCO_FS_ADAPTER_REQUIRED`.
The default filesystem host provider returns a typed denial result, which the
guest reconstructs as `ERR_JCO_FS_ADAPTER_REQUIRED`.
To use the passthrough NodeJS host provider you can map it in:

```console
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ compatibility limits. Related submodules share their parent API page. See the
| [`node:tty`](./tty.md) | Node 24.20 `isatty`, `ReadStream` and `WriteStream` over the host process's descriptors through an explicit host capability; denied by default. |
| [`node:url`](./url.md) | Node 24 URL, URLSearchParams, URLPattern, domain and file conversions; relative file paths use optional WASI environment imports. |
| [`node:util`](./util.md), [`node:util/types`](./util.md) | Portable Node 24 utilities, sharing assertion equality and console formatting. No WIT capability; see the API page for engine and process restrictions. |
| [`node:vfs`](./vfs.md) | Node 26 memory VFS, default-denied Node passthrough, and WASI filesystem storage with configurable preopen placement. |
| [`node:vm`](./vm.md) | Same-context script evaluation and function compilation in the guest. No WIT capability; separate realms, native caches and VM modules are unsupported. |
| [`node:wasi`](./wasi.md) | Node 24.19 `WASI` construction over an explicit host capability, denied by default; `start()` and `initialize()` refuse because a component cannot instantiate a nested module. |
| [`node:worker_threads`](./worker-threads.md) | Real Node host workers over an explicit capability, with guest-local environment data. Native ports, shared memory and profiling are unsupported. |
Expand Down
157 changes: 157 additions & 0 deletions docs/src/interop/nodejs-builtins/supported-modules/vfs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# `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](https://nodejs.org/api/vfs.html#vfscreateprovider-options).
`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()`.

```js
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

| Selection | Host capability | Default behavior |
| --- | --- | --- |
| `--with-nodejs-vfs-via direct` | `jco:node/fs@0.1.0` | Filesystem access is denied with `ERR_JCO_FS_ADAPTER_REQUIRED`. |
| `direct` with an explicit Node host mapping | `jco:node/fs@0.1.0` | Node filesystem passthrough under each `RealFSProvider` root. |
| `--with-nodejs-vfs-via wasi-filesystem` | `wasi:filesystem/preopens` and `types` at `0.2.12` | Access 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:

```sh
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:

```sh
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:

```js
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:

```js
// 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' };
}
```

```sh
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](https://github.com/nodejs/node/tree/f2f2c2f246c36bd74f082cb43ecfe830657d81c9/lib/internal/vfs),
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.
2 changes: 1 addition & 1 deletion packages/jco-std/LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ Software.
The Node.js adaptations identified by upstream provenance comments in
src/wasi/0.2.x/node/24.x.x/stream/, src/wasi/0.2.x/node/24.x.x/test/,
src/wasi/0.2.x/node/24.x.x/util/, src/wasi/0.2.x/node/24.x.x/vm/,
and src/wasi/0.2.x/node/24.x.x/worker-threads/,
src/wasi/0.2.x/node/24.x.x/worker-threads/, and src/wasi/0.2.x/node/26.x.x/vfs/,
and their compiled forms, are covered by the following notice:


Expand Down
24 changes: 24 additions & 0 deletions packages/jco-std/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,30 @@
"./wasi/0.2.x/node/24.x.x/wasi/host/node": {
"types": "./dist/wasi/0.2.x/node/24.x.x/wasi-host-node.d.ts",
"node": "./dist/wasi/0.2.x/node/24.x.x/wasi-host-node.js"
},
"./wasi/0.2.x/node/26.x.x/vfs": {
"types": "./dist/wasi/0.2.x/node/26.x.x/vfs.d.ts",
"browser": "./dist/wasi/0.2.x/node/26.x.x/vfs.js",
"default": "./dist/wasi/0.2.x/node/26.x.x/vfs.js"
},
"./wasi/0.2.x/node/26.x.x/vfs/core": {
"types": "./dist/wasi/0.2.x/node/26.x.x/vfs/core.d.ts",
"browser": "./dist/wasi/0.2.x/node/26.x.x/vfs/core.js",
"default": "./dist/wasi/0.2.x/node/26.x.x/vfs/core.js"
},
"./wasi/0.2.x/node/26.x.x/vfs/impl/wasi-filesystem": {
"types": "./dist/wasi/0.2.x/node/26.x.x/vfs/wasi-filesystem.d.ts",
"browser": "./dist/wasi/0.2.x/node/26.x.x/vfs/wasi-filesystem.js",
"default": "./dist/wasi/0.2.x/node/26.x.x/vfs/wasi-filesystem.js"
},
"./wasi/0.2.x/node/26.x.x/vfs/host": {
"types": "./dist/wasi/0.2.x/node/24.x.x/fs-host.d.ts",
"browser": "./dist/wasi/0.2.x/node/24.x.x/fs-host.js",
"default": "./dist/wasi/0.2.x/node/24.x.x/fs-host.js"
},
"./wasi/0.2.x/node/26.x.x/vfs/host/node": {
"types": "./dist/wasi/0.2.x/node/24.x.x/fs-host-node.d.ts",
"node": "./dist/wasi/0.2.x/node/24.x.x/fs-host-node.js"
}
},
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,6 @@ export const rmdir: FsHost["rmdir"] = (value, options: FsRemoveOptions) =>
capture(() =>
nodeFs.rmdirSync(path(value), {
maxRetries: options.maxRetries,
recursive: false,
retryDelay: options.retryDelay,
}),
);
Expand Down
17 changes: 11 additions & 6 deletions packages/jco-std/src/wasi/0.2.x/node/24.x.x/fs-host.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import type { FsHost } from "./fs/types.js";
import { denyThrow } from "./internal/deny-host.js";
import type { FsHost, FsResult } from "./fs/types.js";

/**
* The default adapter intentionally grants no filesystem capability. Applications must map
* `jco:node/fs@0.1.0` to a host implementation, such as the separately exported Node adapter.
*/
const deny = denyThrow(
"ERR_JCO_FS_ADAPTER_REQUIRED",
"node:fs requires an explicitly configured filesystem host provider",
);
// This interface returns WIT results. Returning its error record keeps denial
// catchable inside a component instead of throwing out of the host trampoline.
const deny = (): FsResult<never> => ({
tag: "err",
val: {
name: "Error",
code: "ERR_JCO_FS_ADAPTER_REQUIRED",
message: "node:fs requires an explicitly configured filesystem host provider",
},
});

export const access: FsHost["access"] = deny;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
*
* - `denyThrow` -- for interfaces whose functions do not return `result`. The provider throws a
* coded `Error`, which jco surfaces to the guest as an exception carrying
* `ERR_JCO_<MOD>_ADAPTER_REQUIRED` (child-process, cluster, console, dns, fs, http).
* `ERR_JCO_<MOD>_ADAPTER_REQUIRED` (child-process, cluster, console, dns, http).
* - fs returns a tagged error result so its denial stays catchable across WIT.
* - Interfaces returning `result<T, record error>` throw serialized error records, which the
* bindings lower into the `err` case; the guest reconstructs the Node error (os).
* - `denyVariant` -- for interfaces whose functions return `result<T, variant error>` with an
Expand Down
16 changes: 16 additions & 0 deletions packages/jco-std/src/wasi/0.2.x/node/26.x.x/vfs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as host from "jco:node/fs@0.1.0";
import { createVfs } from "./vfs/core.js";

const vfs = createVfs(host);

export const create = vfs.create;

export const VirtualFileSystem = vfs.VirtualFileSystem;

export const VirtualProvider = vfs.VirtualProvider;

export const MemoryProvider = vfs.MemoryProvider;

export const RealFSProvider = vfs.RealFSProvider;

export default vfs;
48 changes: 48 additions & 0 deletions packages/jco-std/src/wasi/0.2.x/node/26.x.x/vfs/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Factory overload adapted from nodejs/node v26.8.2, commit
* f2f2c2f246c36bd74f082cb43ecfe830657d81c9, lib/vfs.js (MIT).
* The real provider is injected; memory never consults host capabilities.
*/
import { FsCore } from "../../24.x.x/fs/core.js";
import type { FsHost } from "../../24.x.x/fs/types.js";
import type { HostImports } from "../../24.x.x/internal/wit-types.js";
import { VirtualFileSystem } from "./file-system.js";
import { VirtualProvider } from "./provider.js";
import { MemoryProvider } from "./memory.js";
import { createRealFSProvider } from "./real.js";
import type { RealProviderConstructor } from "./real.js";
import type { VfsOptions } from "./types.js";

export interface VfsModule {
create: typeof create;

VirtualFileSystem: typeof VirtualFileSystem;

VirtualProvider: typeof VirtualProvider;

MemoryProvider: typeof MemoryProvider;

RealFSProvider: RealProviderConstructor;
}

export function create(provider?: VirtualProvider | null, options?: VfsOptions): VirtualFileSystem;
export function create(options: VfsOptions): VirtualFileSystem;
export function create(
provider?: VirtualProvider | VfsOptions | null,
options?: VfsOptions,
): VirtualFileSystem {
if (provider != null && !(provider instanceof VirtualProvider) && typeof provider === "object") {
options = provider;
provider = undefined;
}
return new VirtualFileSystem(provider, options);
}

export function createVfs(
host: HostImports<FsHost> | ((rootPath: string) => HostImports<FsHost>),
): VfsModule {
const RealFSProvider = createRealFSProvider(
(rootPath) => new FsCore(typeof host === "function" ? host(rootPath) : host),
);
return { create, VirtualFileSystem, VirtualProvider, MemoryProvider, RealFSProvider };
}
Loading
Loading