Node Module Loading

Mount a unfs tree into Node itself, so require() and fs.readFileSync serve files straight out of the Merkle DAG.

@platformatic/vfs is a node:fs-compatible virtual filesystem with file descriptors, streams, and the part that makes this worth doing: mount points with module hooks. Mount a subtree and Node's global fs, require(), and import transparently read from it. It reads and writes through a pluggable VirtualProvider, and unfs makes a good one.

Note

Unlike unfs/unstorage and unfs/just-bash, this adapter does not ship with the library. vfs's provider surface is Buffer-shaped, and the only node: imports in src/ are the ones the on-disk store needs. It lives in examples/platformatic-vfs/unfs-provider.ts as a file to copy, around 240 lines with comments.

#How the provider works

vfs drives providers through synchronous primitives, and unfs has a fully synchronous facade, so the mapping is close to 1:1. UnfsProvider holds two facades over the same store and ref:

  • A synchronous UnfsSync (createFsSync) serves every primitive vfs requires: statSync, lstatSync, readdirSync, mkdirSync, rmdirSync, unlinkSync, renameSync, openSync, and the symlink trio. It also overrides readFileSync, writeFileSync, appendFileSync, and copyFileSync directly, short-circuiting the base class's route through a file handle.
  • An async Unfs (createFs), exposed as .unfs, for the content-addressed extras vfs has no notion of.

Both point at one key–value backend, so each sees the other's writes immediately.

unfs's Stats and Dirent are already node:fs-shaped, so they pass straight through. The withFileTypes branch only casts, because vfs's VirtualDirent is a different nominal class with the same shape. The VirtualProvider base class synthesizes everything else (readFile, exists, access, internalModuleStat, and the rest) from those primitives.

#Loading modules from a DAG

import { createRequire } from "node:module";
import { create } from "@platformatic/vfs";
import { initStore, memoryStore } from "unfs";
import { UnfsProvider } from "./unfs-provider.ts";

const store = await initStore(memoryStore());
const provider = new UnfsProvider(store, { now: () => Date.now() });
const vfs = create(provider); // module hooks are on by default

vfs.mount("/app"); // external /app/x → provider (unfs) path /x

vfs.mkdirSync("/app/src", { recursive: true });
vfs.writeFileSync("/app/package.json", JSON.stringify({ name: "demo", main: "src/index.js" }));
vfs.writeFileSync("/app/src/index.js", "module.exports = (who) => `hello, ${who}!`;\n");

const require = createRequire(import.meta.url);
require("/app")("world"); // "hello, world!" — resolved via package.json main, loaded from the DAG
require("node:fs").readFileSync("/app/src/index.js", "utf8"); // the global fs, served by unfs

vfs.unmount(); // the real fs and require are back

The mount window is external only. Paths reach the provider with the prefix stripped, so the same file is /src/index.js to unfs.

await provider.unfs.head(); // the root CID after those writes
await provider.unfs.exportPackBytes("/"); // the package as one CARv1 archive

That last line is the point of the combination. A package Node just loaded is also a hash-verified pack you can move to another store, and every write along the way was a snapshot rather than an overwrite.

#Things to know

  • The store must be synchronous. vfs has no async-provider path, so the provider is built on createFsSync, which needs a synchronous KV driver. memoryStore(), fsStoreSync() and sqliteStore() are the three. An async backend, whether a network KV or unstorage, makes the sync facade throw ENOSYS, and there's no way around that from this side.
  • Module hooks patch process globals. vfs.mount(prefix) patches the live node:fs module and require() / import in place, scoped to paths under the mount, and reverts on unmount(). From an ES module, reach them through createRequire(import.meta.url), since the ESM namespace snapshots the originals and an import * as fs binding won't see the patch. Pass create(provider, { moduleHooks: false }) for the vfs instance API with no global patching.
  • Writes flush eagerly. The file handle persists to the store on every mutation, because unfs snapshots are cheap, so statSync always reflects the latest bytes and closeSync has nothing left to do.
  • copyFileSync is instant. It maps to unfs's own copy, which shares blocks instead of moving bytes.
  • Permissions are recorded, not enforced, and mtimes only appear if you pass a now. Both are properties of unfs rather than of this adapter.

#A runnable example

examples/platformatic-vfs/ builds a small CommonJS package through vfs, require()s it out of the store (both a direct file and a bare directory resolved via package.json "main"), reads the same tree through the async unfs API after unmounting, and exports it as a pack.

unfs  A filesystem you can put in any key–value store.