denoland/deno
ext/node
Active contributors: Yoshiya Hinosawa, Marvin Hagemeister, Bartek Iwańczuk
Purpose
ext/node is Deno's Node.js compatibility layer. It provides every node:* module (node:fs, node:net, node:http, node:crypto, node:child_process, node:os, node:path, node:process, …) by translating their APIs onto Deno's primitives — its ops, the web platform extensions, and the platform abstraction crates.
This is one of the largest subsystems in the repo. The polyfills directory ext/node/polyfills/ holds dozens of TypeScript files that re-implement Node's API surface, often with extensive carve-outs for behavior parity with Node's quirks. Two siblings split the load:
ext/node_crypto— Node's crypto layer (10 Rust files). Big enough to be its own crate.ext/node_sqlite— Node'snode:sqlite(8 Rust files). Similarly split.
Directory layout
ext/node/
├── Cargo.toml
├── lib.rs # extension wiring
├── build.rs # bundles polyfills/ into snapshotted form
├── ops/ # Rust ops backing node-specific APIs
│ ├── …
├── polyfills/ # the JS/TS implementing node:*
│ ├── _next_tick.ts, _process.ts, _stream.mjs
│ ├── async_hooks.ts, buffer.ts, child_process.ts,
│ │ cluster.ts, console.ts, crypto.ts, dns.ts, events.ts,
│ │ fs.ts, http.ts, http2.ts, https.ts, module.ts, net.ts,
│ │ os.ts, path.ts, perf_hooks.ts, process.ts, querystring.ts,
│ │ readline.ts, repl.ts, stream.ts, string_decoder.ts,
│ │ timers.ts, tls.ts, tty.ts, url.ts, util.ts, v8.ts, vm.ts,
│ │ worker_threads.ts, zlib.ts, …
│ └── internal/ # node's "internal" namespace polyfills
├── benchmarks/ # node-shaped microbenchmarks
└── update_node_stream.ts # pulls upstream node:stream code
ext/node_crypto/ # node:crypto (and its sub-modules)
ext/node_sqlite/ # node:sqliteKey abstractions
| Type | Where | Role |
|---|---|---|
NodeExtInitServices |
ext/node/lib.rs |
Bundle of services the extension needs at init: NpmPackageFolderResolver, InNpmPackageChecker, NpmProcessStateProviderRc, FS handle |
NpmPackageFolderResolver |
libs/node_resolver |
Maps require('foo') to the right node_modules/foo/ |
InNpmPackageChecker |
libs/node_resolver |
Tells the runtime whether a module is part of an npm package (and thus subject to Node's resolution rules) |
NpmProcessStateProviderRc |
ext/process (re-exported) |
Provides process.argv, process.env, etc., shared between Deno and Node code |
polyfills/internal/ |
ext/node/polyfills/internal/ |
Mirrors Node's internal/* namespace; the polyfills require('internal/foo') and get the Deno re-implementation |
How it works
graph TD
UserCode["import 'node:fs'<br/>or require('fs')"] --> Resolver["libs/node_resolver"]
Resolver --> Polyfill["ext/node/polyfills/fs.ts<br/>(snapshotted)"]
Polyfill --> Op["op_node_fs_*<br/>ext/node/ops/"]
Polyfill -->|for things already in deno_fs| FsExt["ext/fs"]
Polyfill -->|for streams| WebExt["ext/web (streams)"]
Op --> RustImpl["Rust impl<br/>(typically delegates to deno_fs / std::fs)"]
UserCode2["import 'node:crypto'"] --> Polyfill2["ext/node_crypto/* + polyfills/crypto.ts"]
UserCode3["import 'node:sqlite'"] --> Polyfill3["ext/node_sqlite/*"]Polyfills
Each node:<module> is implemented by one or more files under ext/node/polyfills/<module>.ts. The pattern:
- The polyfill re-uses what's already there:
node:fs.readFilecalls Deno'sop_fs_*ops;node:net.Socketis built onext/net;node:streamusesext/web's streams (with extensive shimming for Node-specific stream behavior). - It adds what's missing: callback-based APIs that Deno doesn't natively expose (
fs.read(fd, ...),net.Socketevents), Node-specific error codes, theinternal/*helper modules. - It shims the differences: error class names,
errnocodes, the difference between Node's Buffer and Deno's Uint8Array.
Some polyfills delegate almost entirely to Deno: node:url is mostly a thin wrapper. Others are nearly from-scratch reimplementations: node:http2, node:tls, node:dns all have hundreds to thousands of lines of polyfill JS.
Resolution rules
When Node code does require('lodash'), the resolver in libs/node_resolver is responsible for finding the right node_modules/lodash/ directory and applying Node's CJS/ESM/conditional-export rules. This is a strict implementation of Node's algorithm, not a paraphrase — interop with the npm ecosystem demands it.
The resolver is plumbed into the runtime via NodeExtInitServices so that ops which load Node modules (e.g., module.createRequire) can answer correctly.
Build-time bundling
ext/node/build.rs runs at compile time to bundle the polyfills directory. Without this step, import 'node:fs' would have to fetch and parse polyfill source from disk on every cold start. The bundling output is snapshotted alongside the rest of the runtime.
Recent commit themes
The recent commit log is dominated by fix(ext/node): entries. This is the area of the codebase with the highest churn at the snapshot date, often fixing edge-case parity bugs against Node's documented or observed behavior. Examples:
fix(ext/node): port internal/priority_queue and expose it via requirefix(ext/node): dns resolveAny with real ANY queryfix(ext/node): rewrite Windows TTY reading to match libuvfix(ext/node): bind to IPv6 wildcard for default Server.listen() to enable dual-stack
Integration points
- Resolver:
libs/node_resolver(Node-style resolution) andlibs/resolver(the unified entry). - CLI:
cli/npm.rsandlibs/npmfor package installation;libs/node_shimtranslates Node CLI args (node script.js -e ...) into Deno-equivalents for thedeno --nodemode. - Runtime:
runtime/worker.rsconstructsNodeExtInitServicesand passes it intodeno_node::deno_node::init_ops_and_esm. - Sibling extensions:
ext/node_crypto,ext/node_sqlite. Other crypto-related code lives there to keep the mainext/nodebuild time reasonable.
Entry points for modification
- New
node:*module or missing API: add a polyfill inext/node/polyfills/<module>.ts. Wire any new ops inext/node/ops/. Look at neighboring polyfills for the pattern. - Crypto-related work:
ext/node_crypto/for the Rust ops,ext/node/polyfills/crypto.tsandext/node/polyfills/internal/crypto/*for the JS surface. node:sqlite:ext/node_sqlite/.- Resolution bug:
libs/node_resolver(algorithm) plus possiblycli/module_loader.rsfor CLI-specific glue.
Testing
- Unit tests for
node:*APIs go intests/unit_node/. - Run as
./x node-test. - The big "compat" suite at
tests/node_compat/runs Node's own test suite. Enabling a new test usually means making a polyfill behave more like Node — see the recenttest: enable parallel/test-...commits for the pattern.
Key source files
| File | Purpose |
|---|---|
ext/node/lib.rs |
Extension registration |
ext/node/build.rs |
Bundles polyfills at compile time |
ext/node/ops/ |
Rust ops backing node-specific APIs |
ext/node/polyfills/ |
The TypeScript implementing node:* |
ext/node/polyfills/internal/ |
Mirror of Node's internal/* |
ext/node_crypto/ |
Crypto-specific extension (10 Rust files) |
ext/node_sqlite/ |
node:sqlite (8 Rust files) |
libs/node_resolver/ |
Node-style module resolution algorithm |
libs/node_shim/lib.rs |
Translates node CLI args into Deno args (5,006 lines) |
tests/node_compat/ |
Node's own test suite, run under Deno |
tests/unit_node/ |
Per-module Deno-authored unit tests |
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.