Host

Connect nstdlib to features supplied by your runtime, such as filesystems, DNS, modules, and globals.

nstdlib runs standard library code outside Node, including in browsers, bundler test environments, and custom JavaScript engines. Some features need services normally provided by an operating system, such as a filesystem, network connection, or source of randomness. Your runtime can provide these features through documented host integrations. This page covers the host APIs you can import and use. See Limits for features that remain unavailable.

#A filesystem: node:vfs

Most of node:fs needs real disk access, which nstdlib does not have. What it does have is node:vfs, a filesystem that lives entirely in memory. Mount one and the whole fs API - sync, callback, fs/promises, streams - works against it:

import vfs from "nstdlib/vfs";
import fs from "nstdlib/fs";
import fsp from "nstdlib/fs/promises";

const mount = vfs.create(); // in-memory by default
mount.mount("/mem");

fs.writeFileSync("/mem/hello.txt", "hi");
fs.readFileSync("/mem/hello.txt", "utf8"); // 'hi'
await fsp.readdir("/mem"); // [ 'hello.txt' ]

mount.unmount(); // and the files are gone

Errors carry real codes and messages (ENOENT: no such file or directory, open '/mem/nope'), and a path outside any mount still throws rather than silently doing nothing. See Limits for the couple of fs operations a memory mount cannot answer.

If you want a filesystem backed by something other than memory - a real disk, an IndexedDB store, a network share - write your own provider. nstdlib/vfs exports the class to extend; three more entries export what your provider has to answer with, because fs compares them by identity rather than by shape:

import { VirtualFileHandle } from "nstdlib/internal/vfs/file_handle"; // what open() returns
import { createENOENT } from "nstdlib/internal/vfs/errors"; // the errno errors you throw
import "nstdlib/internal/vfs/stats"; // createFileStats() and friends, what stat() returns

nstdlib/internal/fs/utils exports vfsState.handlers, the table fs reads a mount out of, and nstdlib/internal/fs/watchers exports the FSWatcher and StatWatcher classes a provider hands back from watch(). These four are lower-level than the memory provider above - reach for them only if the built-in MemoryProvider genuinely does not fit.

#DNS over HTTPS

node:dns resolves real hostnames without a socket, by speaking DNS-over-HTTPS over fetch. It works out of the box, but every lookup goes to a third-party resolver (Cloudflare's, by default), which is a real change in trust worth knowing about and worth being able to change:

import { configureDoH } from "nstdlib/stub/binding/cares_wrap";

configureDoH({ url: "https://dns.google/dns-query" }); // any RFC 8484 resolver
configureDoH({ fetch: myFetch }); // inject the transport
configureDoH({ timeout: 2000 });

process.env.NSTDLIB_DOH_URL sets the endpoint without touching code, and dns.getServers() reports where lookups actually go. If your host can resolve names itself, it can skip DNS-over-HTTPS entirely for dns.lookup() (though not for dns.resolve*(), which asks for DNS records rather than for a way to reach a name) by installing globalThis.node_binding_cares_wrap_getaddrinfo before nstdlib is imported.

#The builtin registry

require() and import() of a bare specifier like require("path") need to know which builtins your program is willing to load - nstdlib does not decide that for you. You register them:

import { BuiltinModule } from "nstdlib/internal/bootstrap/realm";

BuiltinModule.register("path", await import("nstdlib/path"));
BuiltinModule.registerAll({
  os: await import("nstdlib/os"),
  events: await import("nstdlib/events"),
});

Only registered ids resolve. This keeps a program from pulling in the whole library just because it called require("path") once, and it is what both require() and import() read from - see Usage for the full require() walkthrough.

#The ES module loader

import() of a CommonJS module, a JSON file or a registered builtin - over a mount, with the real resolver - runs through a loader you call directly rather than one that intercepts import for you:

import { getOrInitializeCascadedLoader } from "nstdlib/internal/modules/esm/loader";

const loader = getOrInitializeCascadedLoader();
const mod = await loader.import("node:path", "file:///app/index.mjs", {
  __proto__: null,
});
mod.join("a", "b"); // 'a/b'

A builtin still has to be registered first, the same way require() needs it. What this loader cannot do - compile the source text of an actual .mjs file from a mount - is covered in Limits.

#Globals for a non-Node host

Node builds process and Buffer in C++ before any of your code runs. Nothing here does that for you, so on a fresh realm - a browser tab, most of all - the first import "nstdlib/path" fails on process is not defined unless you install them first:

import { installGlobals } from "nstdlib/globals";

await installGlobals(); // process + Buffer, non-enumerable, as Node's are
const { fetch } = await import("nstdlib/internal/deps/undici/undici");

Install before importing anything else, and before importing undici specifically - it reads a free Buffer while its own module body runs. createProcess() is the lower-level export if you want the object without installing it yourself. This is the one host integration whose source is not Node's own code: everything else on this page is Node's standard library, rewritten; nstdlib/globals is written by hand for the part of a realm Node normally builds outside JavaScript entirely.