denoland/deno
Node compatibility
Active contributors: Yoshiya Hinosawa, Marvin Hagemeister, Bartek Iwańczuk
Purpose
Deno aims to run a large fraction of the npm ecosystem unchanged. That means implementing not just the node:* modules but also the small behavioral details that npm packages depend on — error codes, callback shapes, stream backpressure semantics, child-process inheritance rules, etc. The Node compatibility surface is one of the most heavily worked-on areas of the codebase.
This page is a feature-level overview. For implementation details see ext/node.
What "Node-compatible" means here
Three axes:
- Module specifier compatibility —
import "fs",require("fs"),import "node:fs"all work and resolve to the same place. - API compatibility —
node:fs.readFile(path, cb)invokes the callback with the same arguments and the same error shapes Node would. - Behavioral compatibility — observable behavior under stress (concurrency, streams backpressure, signal handling, TTY modes) matches Node.
Axis 1 is owned by libs/node_resolver. Axes 2 and 3 are owned by ext/node and its split-out siblings ext/node_crypto, ext/node_sqlite.
Surface area
The polyfills directory ext/node/polyfills/ contains TypeScript files for every standard node:* module, including:
- Filesystem and IO:
fs,fs/promises,path,os,tty,readline - Networking:
net,tls,http,https,http2,dgram,dns - Streams:
stream,stream/web,stream/promises - Crypto:
crypto, plus the dedicatedext/node_crypto/crate for the heavy lifting - Process:
process,child_process,cluster,worker_threads - Async:
async_hooks,events,timers,perf_hooks,inspector - Compression:
zlib - Module:
module,vm,assert - Misc:
util,url,querystring,string_decoder,console,buffer,repl,v8,domain,punycode,wasi - The
node:sqlitemodule lives in its own crateext/node_sqlite/.
Plus an internal/ subdirectory mirroring Node's own internal/* namespace — which Node's polyfill code uses for shared helpers.
How resolution flows
graph LR
UserCode["require('lodash')"] --> NodeRes["libs/node_resolver"]
NodeRes --> NM["walk node_modules/<br/>or auto-managed npm cache"]
NM --> PkgJson["read package.json<br/>(libs/package_json)"]
PkgJson --> Exports["apply 'exports' / 'main' / conditional exports"]
Exports --> File["resolved .js / .cjs / .mjs file"]
File --> Loader["cli/module_loader.rs"]
Loader --> Run["evaluate"]
UserCode2["require('fs')"] --> NodeRes2["libs/node_resolver"]
NodeRes2 --> Builtin{"is built-in?"}
Builtin -->|yes| Polyfill["ext/node/polyfills/fs.ts"]
Builtin -->|no| NMThe resolver decides whether a specifier is a built-in node:* module, an npm package, or something else, and routes accordingly.
Test infrastructure
tests/node_compat/
This directory imports Node's own test suite (via a submodule) and runs it under Deno. Each Node test is in one of three states:
- Enabled — runs and is expected to pass.
- Disabled — currently fails; tracked in a config file.
- Ignored — irrelevant to Deno (e.g., V8-specific behavior we don't care about, internals).
The recent commit history shows a steady stream of test: enable parallel/... PRs — each one promotes a previously-disabled Node test to "enabled," confirming Deno now passes it. There's a public dashboard at https://node-test-viewer.deno.dev/results/latest.
tests/unit_node/
Deno-authored unit tests for node:* modules. These cover behavior not exercised by Node's own tests, plus regressions found in user reports.
Recent fix patterns
A scan of recent fix(ext/node): commits reveals the kinds of bugs that get filed:
- Callback shape —
cancel pending TLS writes when the socket closes,forward http2 protocol errors from invalid frame callback to session error event - Error code parity —
enforce OpenSSL SECLEVEL key-strength check in createSecureContext,align crypto random* validation with Node - Behavior under specific OS conditions —
rewrite Windows TTY reading to match libuv,bind to IPv6 wildcard for default Server.listen() to enable dual-stack - Edge cases in resolution —
align node:module behavior with Node - Performance —
enable HTTP parser consume fast path
The pattern is roughly: a user report, a minimal repro, a polyfill change in ext/node/polyfills/<module>.ts (sometimes with a Rust op change), and a unit test or node_compat test entry.
--node mode
deno --node script.js is a thin wrapper that translates Node-style CLI args into Deno equivalents. Implemented by libs/node_shim (the lib.rs is 5,006 lines because Node has accumulated many flags). Used by tooling that wants to drop-in replace Node with Deno.
Permissions and Node
Node has no permission model; everything runs with full OS privileges. When you import an npm package, Deno applies its permission model anyway. This means:
fs.readFile('/etc/passwd', cb)from an npm package will fail if--allow-read=/etcwasn't granted.- Network calls go through
--allow-net. child_process.spawn(...)requires--allow-run.
This is intentional. The runtime grants Node-equivalent capability only when the user opts in.
Entry points for modification
- Polyfill bug — almost always
ext/node/polyfills/<module>.ts. Look at the existing functions for the pattern. Add a unit test intests/unit_node/and consider promoting atests/node_compat/entry from disabled to enabled. - New op needed — add to
ext/node/ops/and call from the polyfill. For crypto, prefer adding toext/node_crypto/to keep mainext/nodebuild times reasonable. - Resolution bug —
libs/node_resolver/. These are the trickiest because the algorithm has many edge cases. --nodeflag missing —libs/node_shim/lib.rs.
Key source files
| File | Purpose |
|---|---|
ext/node/lib.rs |
Extension wiring |
ext/node/polyfills/ |
The TS implementing every node:* module |
ext/node/polyfills/internal/ |
Node's internal/* namespace |
ext/node_crypto/ |
Crypto split-out (10 Rust files) |
ext/node_sqlite/ |
node:sqlite |
libs/node_resolver/ |
Node resolution algorithm |
libs/node_shim/lib.rs |
Node-style CLI translator (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.