Usage

Import Node.js builtins from nstdlib and use them across different JavaScript environments.

This page covers everyday use: importing a module, using nstdlib with a bundler, and what to expect in a browser or a worker. To plug in a filesystem, a network or other features your runtime provides, see Host Integration.

#Import a builtin

Each Node builtin has its own import path, named after the module:

import path from "nstdlib/node/path";
import { EventEmitter } from "nstdlib/node/events";

const emitter = new EventEmitter();
emitter.on("greet", (name) => console.log(`hello, ${name}`));
emitter.emit("greet", "world");

console.log(path.join("a", "b", "c")); // 'a/b/c'

Like Node's own builtins, most modules have both a default export and named exports, and they point to the same functions. Both of these work:

import fs from "nstdlib/node/fs";
import { readFileSync } from "nstdlib/node/fs";

Tip

In a browser or other non-Node environment, modules create a default process if there isn't one. To set its working directory, environment or output streams, call installGlobals() from nstdlib/globals before importing any module. See Host Integration.

#What works out of the box

Modules that are pure JavaScript, like path, events, buffer, util, url, assert and stream, work anywhere, because they never needed an operating system.

Two more work without any setup, because they use standard web APIs:

import crypto from "nstdlib/node/crypto";
import dnsPromises from "nstdlib/node/dns/promises";

crypto.randomUUID(); // uses the runtime's built-in WebCrypto
await dnsPromises.resolveMx("gmail.com"); // a real DNS answer, over HTTPS
  • Randomness and WebCrypto (crypto.randomBytes(), randomUUID(), crypto.subtle) use the globalThis.crypto every modern runtime has.
  • DNS (dns.lookup(), dns.resolve*()) uses DNS-over-HTTPS through fetch. See Host Integration to choose the resolver.
  • Hashing (createHash(), createHmac(), crypto.hash()) is computed in JavaScript, so it works everywhere too.

#What needs your runtime's help

Anything that needs a real filesystem, sockets, child processes or OpenSSL can't work on its own in a browser or a worker. Calling it doesn't hang or return undefined. It throws an error that names what's missing:

Error: [nstdlib] internalBinding('fs').open() is not implemented: this
runtime has no such binding, and no host implementation is installed on
`globalThis.node_binding_fs_open`.

You can often fill the gap yourself. For example, installFileSystem() gives fs an in-memory filesystem, and installNetwork() lets net and http run inside the page. The Host Integration page covers all of these, and Limitations covers what can't be filled.

Check a module's page in the API reference before relying on it.

#require() needs eval

require() compiles CommonJS modules with new Function(). If your runtime blocks that, for example a page with a Content-Security-Policy that doesn't allow unsafe-eval, require() isn't available at all. The same goes for import() of a CommonJS module. Importing a builtin or a JSON file works either way.

#Run a whole program

nstdlib/embed runs Node programs rather than importing modules one at a time. You create a world (files, a working directory and an environment), start a node on it, and run programs the way you would type node in a shell:

import { createNode, createWorld } from "nstdlib/embed";

const world = await createWorld({
  files: { "hello.js": "console.log(require('os').platform())" },
});
const node = await createNode({ world, engine: "js" });

const { code, stdout, stderr } = await node.run({ argv: ["hello.js"] });

It works in a page, a worker or Node, and it sets up process, Buffer and the filesystem for you, so don't import any nstdlib/node/* module before createWorld() has finished. After that, world.fs and world.files read and write the same files the program sees.

  • engine: "js" runs the program in your own JavaScript engine. Only the builtins an ordinary script uses are loaded by default; pass builtins: "all" (or a list of ids) when a program needs http2, tls, sqlite or the test runner.
  • engine: "wasm" runs it in nstd.wasm, a separate runtime compiled to WebAssembly, which isn't part of this package. Pass it as module (a URL, a Response, the bytes or a compiled WebAssembly.Module). Without one, createNode() throws an EngineUnavailable error with reason: "missing-module".
  • child_process, worker_threads and vm need a host. Under Node, nodeCommand(), nodeWorkers() and nodeVm() are ready-made ones; pass them to createWorld() as commands, workers and realms.

createWorld() takes a provider as well, for a filesystem of your own. A function there is called with the realm's nstdlib/node/vfs once it is set up, which is how you build a provider from its MemoryProvider or VirtualProvider without importing that module too early.

#Use it with a bundler

nstdlib/node/* modules are plain ES modules, so any modern bundler (Vite, Rolldown, webpack, esbuild, Rollup) can import, split and tree-shake them like any other package.

nstdlib doesn't redirect node: imports for you, but because each import path matches the builtin's name, you can do it with a simple alias in your bundler config:

// vite.config.js
export default {
  resolve: {
    alias: {
      "node:fs": "nstdlib/node/fs",
      "node:path": "nstdlib/node/path",
      "node:events": "nstdlib/node/events",
    },
  },
};

Only alias the modules your code actually uses. An alias always resolves, but if the module doesn't support what your code needs in your environment (see the API reference), the call will still fail at run time.

nstdlib/embed needs no configuration: every module it loads is imported by name, so a bundler finds and splits them like the rest of your code.