
# Host Integration

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

In Node, the standard library relies on the operating system for things like
files, sockets and processes. Outside Node, nstdlib has none of those by
default. This page shows how to plug them in, either with the ready-made
helpers in `nstdlib/globals` or with your own implementation.

| You want                       | Use                                                 |
| ------------------------------ | --------------------------------------------------- |
| `process` and `Buffer` globals | [`installGlobals()`](#process-and-buffer)           |
| A working `fs`                 | [`installFileSystem()`](#a-filesystem)              |
| Working `net` and `http`       | [`installNetwork()`](#a-network)                    |
| A different DNS resolver       | [`configureDoH()`](#dns)                            |
| Working `child_process`        | [`installProcesses()`](#child-processes)            |
| `import` of `.mjs` files       | [`nstdlib/esm`](#es-modules)                        |
| `require("path")` and friends  | [`BuiltinModule.register()`](#the-builtin-registry) |

Features that can't be plugged in at all are listed in
[Limitations](/library/limitations).

> [!IMPORTANT]
> Several of these helpers must run **before** you import the module they
> affect, because that module checks for them once, when it loads. The safest
> order is: install everything first, then import `nstdlib/node/*` modules
> with `await import()`.

## `process` and `Buffer`

Node creates `process` and `Buffer` in C++ before any of your code runs. A
browser or worker has neither. nstdlib's modules don't need you to add them:
if there's no global `process` when the first module loads, nstdlib creates a
default one and installs it on `globalThis`. That default says the platform is
`"browser"`, the working directory is `/`, the environment is empty, and
anything written to stdout or stderr is dropped.

To give `process` your own values, call `installGlobals()` before importing
any `nstdlib/node/*` module. It also adds a global `Buffer`, which code you run
on top of nstdlib may expect:

```js
import { installGlobals } from "nstdlib/globals";

// adds process and Buffer, non-enumerable like Node's
await installGlobals(globalThis, { cwd: "/app", env: { HOME: "/app" } });

const path = await import("nstdlib/node/path");
```

It only adds what's missing, so it never replaces an existing `process`. That
includes the default one: once any `nstdlib/node/*` module has loaded, a later
`installGlobals()` call keeps the default and ignores the values you pass. If
you want the `process` object without installing it as a global, use
`createProcess()` instead.

## A filesystem

Most of `fs` needs a disk, which nstdlib doesn't have. `installFileSystem()`
gives it one. Pass it a provider from `node:vfs`, such as `MemoryProvider`,
which keeps files in memory. After that, the whole `fs` API works on it: sync,
callback and promise functions, streams, and `require()` and `import()`.

```js
import { installFileSystem } from "nstdlib/globals";
import { MemoryProvider } from "nstdlib/node/vfs";
import fs from "nstdlib/node/fs";
import fsp from "nstdlib/node/fs/promises";

const mem = await installFileSystem(globalThis, new MemoryProvider(), {
  mount: "/mem", // defaults to "/"
});

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

mem.dispose(); // unmount; the files are gone
```

A few things worth knowing:

- **Order doesn't matter here.** You can install a filesystem before or after
  importing `fs`.
- **You can mount several.** Each handles paths under its own mount point. A
  path no mount handles still throws an error rather than silently doing
  nothing.
- **Errors look like Node's**, with real codes and messages such as
  `ENOENT: no such file or directory, open '/mem/nope'`.
- **File descriptors are plain numbers** starting at `3`.

If you run several programs one after another in the same page, `mem.scope()`
helps you clean up `fs.watch()` watchers between them. It tracks the watchers
started after you call it. Its `holding` property counts the ones that would
keep a Node process alive (so not those created with `persistent: false` or
`unref()`), and `close()` stops them the way a process exit would.

### Write your own provider

To keep files somewhere other than memory, such as IndexedDB, a real disk or a
network share, extend `VirtualProvider` from `nstdlib/node/vfs` and install it
the same way. Your provider must return the same classes `fs` expects, so
import them from these modules:

```js
import { VirtualProvider } from "nstdlib/node/vfs"; // the class to extend
import { VirtualFileHandle } from "nstdlib/node/internal/vfs/file_handle"; // what open() returns
import { createENOENT } from "nstdlib/node/internal/vfs/errors"; // errors to throw
import "nstdlib/node/internal/vfs/stats"; // createFileStats() and friends, for stat()
```

Some tips for writing one:

- **Paths are relative to the mount point**, so the same provider works at
  `/` or at `/mem`.
- **Sync methods are enough.** If you only implement the `*Sync` methods, the
  callback and promise forms of `fs` fall back to them. You can also implement
  the async methods separately; `fs.readFile()` and `fs.promises.readFile()`
  call your async `readFile()` when you have one.
- **Some methods are optional.** Without `chmodSync`, `chownSync`,
  `utimesSync` (or their `l` versions), those calls fail with `ENOSYS`.
- **Watching is optional too.** If your provider has a `watch()` method,
  `fs.watch()` uses it. Otherwise nstdlib polls your provider with
  `statSync()` and `readdirSync()`.

If you're extending `node:vfs` itself, two lower-level modules are also
published: `nstdlib/node/internal/fs/utils` exports `vfsState.handlers`, and
`nstdlib/node/internal/fs/watchers` exports the `FSWatcher` and `StatWatcher`
classes.

> [!NOTE]
> `node:vfs` on its own still works as in Node. Since Node v26.9,
> `vfs.create().mount()` takes no arguments and mounts at a reserved path of
> its own (`/dev/null/vfs/0` on POSIX). To mount a filesystem at a path you
> choose, use `installFileSystem()`.

## A network

`net` and `http` are fully implemented in nstdlib: connections, servers,
backlogs, half-closed sockets and so on. What's missing is the socket itself.
Without one, `listen()` and `connect()` fail with `ENOSYS`.

`installNetwork()` provides an in-memory network that lives inside your
JavaScript environment. Call it before the first import of `nstdlib/node/net`
(or anything that uses it, such as `http`):

```js
import { installGlobals, installNetwork } from "nstdlib/globals";

await installGlobals();
await installNetwork();

const http = await import("nstdlib/node/http");
http.createServer((req, res) => res.end("hi")).listen(8080);
```

Now `http.request()`, `net.connect()` and `net.createServer()` work between
servers and clients in the same page. That's enough to run code written for
`node:http` with nothing underneath it.

If your runtime already has a real network (for example, in nstd),
`installNetwork()` does nothing and returns `null`.

> [!WARNING]
> This is **not** a real network. It can't reach anything outside the page.
> Connecting to an address that no server in the page is listening on fails
> with `ECONNREFUSED`.

### Reach the outside world

To connect to real servers, pass a `provider`. Its `connect()` method is
called whenever code connects to an address nothing in the page is listening
on. Return `null` to refuse the connection, or return a channel with `write()`,
and optionally `end()` and `close()`. For example, through a WebSocket relay:

```js
const network = await installNetwork(globalThis, {
  provider: {
    connect(address, port, sink) {
      const socket = new WebSocket(`wss://relay.example/${address}/${port}`);
      socket.binaryType = "arraybuffer";
      socket.onopen = () => sink.open();
      socket.onmessage = (e) => sink.data(new Uint8Array(e.data));
      socket.onclose = () => sink.end();
      socket.onerror = () => sink.error("ECONNRESET");
      return { write: (bytes) => (socket.send(bytes), true) };
    },
  },
});
```

A WebTransport relay, a WebRTC data channel or your own network stack plugs in
the same way.

The returned `network` also works in the other direction. `network.dial()`
lets code outside the page connect to a server inside it. For example, a
service worker can send the page's `fetch` requests to an in-page
`http.createServer()`:

```js
const channel = network.dial("127.0.0.1", 8080, sink);
channel.write(requestBytes);
```

`network.endpoints` lists what's currently listening, and
`network.descriptors` shows how many sockets are open.

## DNS

`node:dns` works without any setup by sending DNS-over-HTTPS requests with
`fetch`. By default, lookups go to Cloudflare's public resolver. That means a
third party sees every hostname you look up, so you may want to change it:

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

configureDoH({ url: "https://dns.google/dns-query" }); // any RFC 8484 resolver
configureDoH({ fetch: myFetch }); // use your own fetch
configureDoH({ timeout: 2000 }); // in milliseconds
```

You can also set the resolver with the `NSTDLIB_DOH_URL` environment variable
(`process.env.NSTDLIB_DOH_URL`), and `dns.getServers()` shows which resolver is
in use.

If your runtime can resolve hostnames itself, define
`globalThis.node_binding_cares_wrap_getaddrinfo` before importing nstdlib, and
`dns.lookup()` will use it instead of DNS-over-HTTPS. The `dns.resolve*()`
functions still use DNS-over-HTTPS, because they ask for specific DNS records.

## Child processes

`installProcesses()` makes `spawn()`, `exec()`, `execFile()` and `fork()` work
on top of whatever your application considers a "command". You provide a
`CommandHost` object whose `spawn()` method starts a command and returns its
stdio and exit status. nstdlib doesn't include one; what a command means is up
to you. The expected shape is defined in
[`src/embed/types/commands.ts`](https://github.com/unjs/nstd/blob/main/src/embed/types/commands.ts).

```js
import {
  installGlobals,
  installNetwork,
  installProcesses,
} from "nstdlib/globals";

await installGlobals();
const network = await installNetwork();
await installProcesses(globalThis, myCommandHost, { network });

const { spawn } = await import("nstdlib/node/child_process");
```

A few rules:

- **It needs `installNetwork()`.** A child's stdin, stdout and stderr travel
  over the in-memory network, so pass its return value as `network`.
- **Install it before** the first import of `nstdlib/node/child_process`.
- **`fork()` messages can go both ways** if your host's child object has a
  `write(fd, bytes)` method. Without it, messages only travel from child to
  parent, and `send()` from the parent fails with an error.
- **`spawnSync()` and the other sync functions aren't supported.** They still
  throw.

## ES modules

nstdlib can't compile `.mjs` source on its own: JavaScript has no standard way
to compile a module while letting you control how its imports resolve.
`nstdlib/esm` fills that gap with a compiler that transforms each module into
a function. Install it like this:

```js
import { compileSourceTextModule } from "nstdlib/esm";

globalThis.node_binding_module_wrap_compileSourceTextModule =
  compileSourceTextModule;
```

After that, Node's own ES module loader can load `.mjs` files from an
installed filesystem, with the real resolver. You can call the loader
directly:

```js
import { getOrInitializeCascadedLoader } from "nstdlib/node/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'
```

Like `require()`, this compiles code from strings, so it needs a runtime that
allows `new Function()`. Builtins still have to be registered first, as
described below.

`nstdlib/esm` also exports `createLoader()`, a smaller standalone loader
that takes your own `readSource()`, `resolve()` and `builtin()` functions.

## The builtin registry

`require()` and `import()` of a builtin, such as `require("path")`, only work
for builtins you register. nstdlib doesn't register any by default, so a
program doesn't pull in the whole library just because it asked for one
module:

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

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

## More advanced hooks

`nstdlib/globals` has a few more helpers for embedders:

- **`installNodeProcess()`** adds the rest of Node's `process` to the default
  one: `cpuUsage()`, `kill()`, `execve()`, a validating `emitWarning()` that
  prints, `process.binding()`, `features`, `allowedNodeEnvironmentFlags`,
  `setSourceMapsEnabled()` and `getBuiltinModule()`, each Node's own function
  over nstdlib's bindings. Call it after `installGlobals()` when you run whole
  programs rather than import a module or two. A method with nothing behind it
  throws and names the `globalThis.node_binding_process_methods_*` hook that
  would back it; `umask()`, `chdir()` and `cwd()` are replaced only if you
  installed those hooks first. It does nothing to a `process` nstdlib didn't
  create.
- **`installParser()`** loads the JavaScript parser, which TypeScript
  type-stripping (`.ts` files) needs. It's optional because the parser is the
  largest part of the package.
- **`installWorkers(globalThis, host)`** makes `worker_threads` work, given a
  host that can run a second JavaScript environment on another thread. See
  [`src/embed/types/workers.ts`](https://github.com/unjs/nstd/blob/main/src/embed/types/workers.ts)
  for the shape.
- **`installRealms(globalThis, host)`** makes `vm.createContext()` work, given
  a host that can create a separate global scope, such as an `<iframe>`. See
  [`src/embed/types/vm.ts`](https://github.com/unjs/nstd/blob/main/src/embed/types/vm.ts)
  for the shape.

Like the others, each one leaves an existing implementation alone, so they're
safe to call in nstd or under Node.
