node-compat modules

The built-in modules Lifo implements — fs, http, crypto, stream, child_process, process, and more — and the behaviors that matter.

require("node:*") (and the bare aliases) resolve to Lifo's own implementations, written against the kernel. They aren't the real Node modules and they aren't WASM — they're TypeScript that reproduces the observable behavior real packages depend on.

What's implemented

  • Filesystemfs and fs/promises over the VFS: readFile/writeFile, readdir (with withFileTypes), stat/lstat, mkdir, rm/rmdir, open flags, createReadStream/createWriteStream, and watch/watchFile (returning a real StatWatcher) wired to VFS watch events.
  • Networkinghttp/https (servers that register on the port registry, and a client that goes through the proxying fetch), plus net/tls sockets and a dns resolver. dgram is a no-op shim (enough for tools that probe mDNS).
  • Process & OSprocess (argv, env, cwd, exit, stdio, hrtime, nextTick), os (platform() returns "lifo", cpus() returns 1 so worker-forking tools run in-band), child_process (exec/execFile/spawn bridged to real VM commands via executeCapture).
  • Streams & encodingstream (Readable/Writable/Transform, finished, pipeline, web-stream interop), stream/promises, string_decoder, buffer (a Buffer whose subarray/slice share memory like Node's), zlib.
  • Crypto — hashing (md5, sha family), randomBytes, and digest encodings including base64url (which ubiquitous tools like write-file-atomic depend on for temp filenames).
  • Runtime plumbingevents (a function-constructor EventEmitter, so EventEmitter.call(this) works), util (+types, debuglog), readline (with real keypress parsing), tty, timers (returning Timeout objects with ref/unref/refresh), module (a constructable Module class), async_hooks, diagnostics_channel, console, assert, url, path.

Why the small behaviors matter

Most of the effort in the compat layer is in the details that a real package trips over. A few representative examples:

  • Buffer#subarray must share memory (like Node), or msgpackr's encodeInto writes into a throwaway buffer and Metro's transform cache decodes garbage.
  • crypto digests must support base64url, or atomic file writes name their temp file with binary garbage and every write ENOENTs.
  • child_process stdout must emit close after end, or Metro's file crawler waits forever and the first bundle hangs.
  • os.cpus() returning 1 makes jest-worker/Metro run in-band, avoiding a fake child_process fork that would hang.

These aren't hacks so much as the actual contract of the Node API. Getting them right is what lets create-expo-app, Vite, and Metro run unmodified.

Networking specifics

Because a browser tab can't make arbitrary cross-origin requests, the compat layer routes outbound HTTP through a CORS proxy for known non-CORS hosts (e.g. api.expo.dev). This is injected as the fetch a module receives, and require("fetch-nodeshim") is served from the same stack, so a package's networking is transparently proxied without overriding globalThis.fetch. See Networking and Tunnelling.

Note

The surface grows as real tools demand it. The roadmap extends it toward serverless handlers and broader ecosystem coverage, and adds pluggable WASM runtimes for the cases that genuinely need a native binary (ffmpeg, Python).