Node.js-compatible runtime running transformed code in a Worker with node globals.
NodeWorker.create(workerURL, puterToken, cwd, options) is the entry point. The second
argument decides which of the two starts you get: a puter token makes the worker that user's,
and an empty one makes it anonymous — nothing in the runtime calls api.puter.com, and the
filesystem and network are whatever you supply instead.
swURL is needed either way: the module resolver is synchronous end to end, so without the
service worker backing synchronous fs there is no working require and nothing runs.
Pass requireSyncFs: false only if you genuinely intend to run on fs.promises alone.
import { NodeWorker } from "node-worker";
import workerURL from "node-worker/worker?url";
import swURL from "node-worker/sw?url";
const worker = await NodeWorker.create(workerURL, puterToken, "/project", {
swURL,
});
await worker.import("/project/index.js", {
argv: ["node", "/project/index.js"],
});import { NodeWorker } from "node-worker";
import workerURL from "node-worker/worker?url";
import swURL from "node-worker/sw?url";
const worker = await NodeWorker.create(workerURL, "", "/project", {
swURL,
// Optional. Without it the worker has no network at all.
net: {
wispUrl: "wss://anura.pro/",
peerToken: localStorage.getItem("peer") ?? crypto.randomUUID(),
},
});
// Nothing is mounted under "/" but memory, so put the code there yourself.
const project = worker.vfs.mountMemory("/project");
project.write([
{ path: "package.json", data: pkgJson },
{ path: "index.js", data: src },
]);
await worker.import("/project/index.js", {
argv: ["node", "/project/index.js"],
});What differs from the authenticated start:
- The filesystem is yours to provide. The root is the memory overlay with nothing under it, so a fresh anonymous worker has an empty
/and a memory/tmp. Populate it withworker.vfs.mountMemory(...), or mount a real backend likecreateDirectoryHandleProviderover a File System Access handle, or your ownVfsProviderfor OPFS/IndexedDB/a fetch. - The network comes from
options.net. Any Wisp-compliant relay works. fs.watchonly sees local mutations. Mutations made through this worker's own providers still reach watchers.
An authenticated worker puts a read cache in front of puterfs — stats, directory listings, negative lookups and file contents — so a repeated read costs nothing and a directory listed in full answers every miss under it locally. It is on by default, and it is only sound because the same token buys a change feed to invalidate it with: puterfs's socket names every path that moves, and when the socket is unavailable (an app launched with an app token cannot authenticate one) the runtime falls back to polling the account's change counter, which says that something moved without saying what.
new NodeVfs({
puter: {
token,
cache: {
maxBytes: 64 * 1024 * 1024, // file contents held; 0 caches metadata only
maxFileBytes: 8 * 1024 * 1024, // larger files stream through uncached
maxStaleMs: 3000, // tolerated window of unreported change
enabled: true,
},
},
});vfs.cacheStats() reports hits, misses and bytes held, alongside apiStats() (what left the
browser) and opStats() (what the worker asked for) — the three together are where a run's
filesystem traffic actually went. vfs.flushCache() drops it all.
Other mounts can have one too, via mount(root, provider, { cache }). It is on by default for
a read-only mount, since nothing can write through one, and off otherwise — there is no
change feed for a FileSystemDirectoryHandle, so "nobody else touches this" is a claim only
the consumer can make.