nodejs/node
github.com/nodejs/node ↗ · 3999 files · 43.3 MB · scan capped
Node.js Node.js is an open-source, cross-platform JavaScript runtime environment.
Overview
This is the Node.js runtime itself at v26.10.0 — a C++/V8 host (src/, ~495 files) wrapping a JavaScript standard library (lib/, ~429 files) that is compiled into a V8 startup snapshot at build time. Beyond upstream-familiar surface, this tree carries several large experimental subsystems wired through the same plumbing: a `node:vfs` virtual file system that intercepts `node:fs` and the module loaders at reserved mount points under `os.devNull/vfs/<id>`, a `node:ffi` libffi-backed foreign function interface gated by a `kFFI` permission scope, a `node:dtls` endpoint/session stack, a `node --experimental-bench` benchmark runner mirroring the test runner, and a vendored Rust crate set (`deps/crates`, temporal_rs + ICU) linked as a staticlib. The audience is Node core collaborators; the distinctive engineering property is that every new capability is a triple — a CLI option in `src/node_options.cc`, a `setupX()` in `pre_execution.js` that calls `BuiltinModule.allowRequireByUsers()`, and an internal binding registered in `src/node_binding.h`.
System map
Execution begins in `src/node_main.cc` (`main`/`wmain`) which calls `node::Start`, which after SEA argv fixup loads the V8 startup snapshot and runs a `NodeMainInstance`. C++ then picks exactly one main script from `lib/internal/main/` via a linear dispatch in `src/node.cc`; that script calls into `lib/internal/process/pre_execution.js` to configure the realm before user code. The JS layer talks to C++ only through `internalBinding()`, whose registry is the `NODE_BUILTIN_*_BINDINGS` macro list in `src/node_binding.h`. Build is GYP/Make (`configure.py` → `node.gyp` → `Makefile`) with a secondary GN path, plus Cargo for `deps/crates`. There is no deployment target in the usual sense: the artifact is the `node` binary, optionally repackaged as a single executable application by `src/node_sea.cc`.
- The process entrypoint is `main()` (POSIX) / `wmain()` (Windows, which UTF-8-converts wide argv and refuses pre-Windows-10 unless `NODE_SKIP_PLATFORM_CHECK=1`), both delegating to `node::Start`.
- `node::Start` first runs `sea::FixupArgsForSEA` so a single-executable build can rewrite its own argv, then calls `StartInternal`, which branches to `sea::BuildSingleExecutable`/`WriteSingleExecutableBlob` or `GenerateAndWriteSnapshotData` before falling through to `LoadSnapshotData` + `NodeMainInstance::Run`.
- C++ selects the JS main script by an ordered if-chain in `src/node.cc`: worker_thread → inspect → print_help → prof_process → eval_string → check_syntax → bench_runner → test_runner → watch_mode → run_main_module → repl → eval_stdin. Adding a new run mode means adding a branch here plus a file in `lib/internal/main/`.
- The JS↔C++ boundary is the `internalBinding()` registry declared by macro lists in `src/node_binding.h`; optional subsystems are compiled in conditionally — `quic` behind `HAVE_OPENSSL && OPENSSL_NO_QUIC != 1`, `dtls` behind `HAVE_OPENSSL && HAVE_DTLS`, `sqlite`/`webstorage` behind `HAVE_SQLITE`, `ffi` behind `HAVE_FFI`.
- `lib/internal/bootstrap/node.js` is executed by `Realm::BootstrapRealm()` and is normally *not* run at startup — its resulting V8 heap is captured into the build-time snapshot by `SnapshotBuilder::Generate()`; `--no-node-snapshot` forces real execution. It must stay synchronous and must not read argv or env at top level.
- `prepareExecution()` in `pre_execution.js` is the single funnel every main script passes through; it patches the process object, runs ~25 `setupX()` initializers, calls `initializePermission()`, initializes the module loaders, and mounts the VFS before returning the resolved main entry.
- The storage layer is threefold: the real file system via the `fs`/`fs_dir`/`glob` bindings, SQLite via `src/node_sqlite.cc` (which now exposes virtual tables through a `VirtualTableModule` wrapping `sqlite3_module`), and Web Storage via the `webstorage` binding gated on `--experimental-webstorage`.
- External native dependencies are vendored under `deps/` (v8, uv, openssl, ada, llhttp, nghttp2, ngtcp2, simdjson, sqlite, undici, zlib, zstd, libffi, LIEF, perfetto, postject) rather than fetched, so builds are hermetic.
- `deps/crates` is a Rust staticlib (`crate-type = ["staticlib"]`, `node_crates` v14.6.202, rust-version 1.86) vendoring `temporal_rs`/`temporal_capi` and the ICU4X crates; its Cargo.toml is generated by node-core-utils and must not be hand-edited.
- Build/test commands: `./configure && make -j4`, `make test` (tooltest + addon builds + cctest + jstest), `make test-only` (skips doc build), `make test-ci` for CI, `make lint` (js/cpp/md/yaml), `make coverage`.
- `configure.py` owns feature toggles that become gyp variables — `--shared-ffi`/`--without-ffi` set `node_use_ffi`, and `--v8-enable-temporal-support` gates the Rust temporal path; `node.gyp` defaults `node_use_ffi%` to `false` and only pulls in `deps/libffi/libffi.gyp:libffi` when it is true.
- Type checking of `lib/` is opt-in and non-emitting: `tsconfig.json` sets `allowJs: true`, `checkJs: false`, `noEmit: true`, excludes `src`/`tools`, and maps every public builtin id to its `lib/*.js` file plus `internal/*` to `lib/internal/*`.
Feature catalog
User-facing surface is the CLI plus the `node:*` builtin module set. The CLI's 'subcommands' are really the run-mode branches in `src/node.cc` — `node inspect`, `node --test`, `node --experimental-bench`, `node --watch`, `node -e`, `node --check`, `node --prof-process`, `node --build-snapshot`, `node --build-sea`, and the default script/REPL/stdin paths. Module features are individually flag-gated and only become requirable when a `setupX()` in `pre_execution.js` calls `BuiltinModule.allowRequireByUsers()`. This tree's notable additions over a stock runtime are `node:vfs`, `node:ffi`, `node:dtls`, `node:bench`, `node:quic`, `node:sqlite` with virtual tables, and `stream/iter`/`zlib/iter`.
- `node:vfs` exposes `create()`, `registerProvider()`, and the classes `VirtualFileSystem`, `VirtualProvider`, `MemoryProvider`, `RealFSProvider`, `ZipProvider`; it is `node:`-scheme-only and requires `--experimental-vfs`.
- `--vfs-mount <src>` (repeatable) and `--vfs-load <src>` both append to the same `vfs_mounts` option vector; `--vfs-load` additionally sets the `[vfs_load_set]` implied bool so the entry point can be taken from inside a mount.
- `node:ffi` provides `DynamicLibrary` plus typed pointer accessors (`getInt8`…`getFloat64`, `setInt8`…, `exportBytes`, `getRawPointer`, `kFastArguments`) and emits an experimental warning at module load.
- `node:bench` mirrors the test runner's API shape — `bench`, `suite`/`describe`, `before`/`after`/`beforeEach`/`afterEach`, `run`, `runFile`, `createRunner` — and is reached from the CLI by `node --experimental-bench`, which routes to `internal/main/bench_runner`.
- `node:dtls` exposes `connect`, `listen`, `createSecureContext`, and `DTLSEndpoint`/`DTLSSecureContext`/`DTLSSession`, backed by `src/dtls/{dtls,dtls_context,dtls_endpoint,dtls_session}.cc`; `DTLSEndpoint` manages a UDP socket and routes datagrams to sessions.
- The permission model (`--permission`) exposes `process.permission.has()` / `.drop()` and nine scopes: fs (with read/write children), child, wasi, worker, inspector, net, addon, ffi, openssl.store.
- `--permission-audit` is a distinct mode from `--permission`: `isAuditMode()` reads it separately and `Permission::EnableWarningOnly()` makes denials warn rather than throw.
- Single-executable applications are configured by a JSON file parsed in `src/node_sea.cc` supporting `useSnapshot`, `useCodeCache`, `assets`, and `disableExperimentalSEAWarning`; assets are serialized into the blob and read back as `string_view` pairs.
- SEA assets can be surfaced as a read-only mounted VFS via `initSeaVfs()`, which places the SEA main script at the mount-point root so `__dirname`-relative `require()` calls inside a bundled app resolve to bundled assets.
- `node --watch` is implemented as a supervisor process: `internal/main/watch_mode.js` strips watch flags into `argsWithoutWatchOptions`, re-`spawn`s `process.execPath`, and drives restarts from a `FilesWatcher` with a 200 ms debounce in `filter` or `all` mode.
- The test runner CLI parses options via `parseCommandLine()`, supports `--test-isolation=none` (which defers user-module loading into `run()`), forces concurrency 1 when the inspector is active, and sets a nonzero exit code from the `test:summary` event.
- `--experimental-stream-iter` unlocks two additional builtins in one shot, `stream/iter` and `zlib/iter`, via the same `allowRequireByUsers` mechanism.
Data flow
Input boundaries are argv/env (parsed into `EnvironmentOptions` by `src/node_options.cc` and read from JS via `getOptionValue()`), the file system, sockets, and the SEA blob embedded in the binary. Options are the master state: essentially every JS feature branch keys off `getOptionValue('--flag')`, and options are re-read after deserialization by `refreshRuntimeOptions()`. The heaviest transform in this tree is the VFS path router, which reroutes `node:fs` calls and module-loader fs calls whose resolved path falls under a reserved mount prefix, before those calls reach the native `fs` binding. State owners are the `Environment` object on the C++ side and, on the JS side, module-level singletons: `vfsState.handlers` in `internal/fs/utils.js`, the `loaderOverrides` map in `internal/modules/helpers.js`, the `providers` array in `provider_registry.js`, and `Module._cache`/`_pathCache` in the CJS loader.
- CLI/NODE_OPTIONS parsing populates `EnvironmentOptions` fields declared in `src/node_options.h` (`vfs_mounts`, `experimental_vfs`, `vfs_load`, `allow_fs_vfs`) which JS then reads through `internal/options`'s `getOptionValue()`.
- `refreshRuntimeOptions()` is the first call inside `prepareExecution()`, re-reading options after snapshot deserialization so a snapshotted heap does not carry stale build-time option values.
- Every VFS mount is assigned a numeric layer id and is served at `${os.devNull}/vfs/<id>`; because `os.devNull` is a character device (`/dev/null`, `\\.\NUL`) it can have no children, so path ownership is decidable from the string alone without touching the disk.
- `getLayerIdFromPath()` parses the layer id with a hand-rolled digit scan over char codes 47/92 (`/`, `\`) rather than splitting — a deliberate hot-path optimisation since it runs on candidate fs calls.
- Dispatch into the VFS is a single null-check per fs function: `lib/fs.js` reads `vfsState.handlers` and, if non-null, calls the handler and routes the returned promise through `vfsResult()`/`vfsVoid()` to the error-first callback; when no VFS is mounted the field is `null` and overhead is one comparison.
- `setVfsHandlers()` is the only writer of `vfsState.handlers` and is imported by `internal/vfs/setup.js`, making that file the single install point for fs interception.
- Module resolution is redirected separately from `fs`: `internal/modules/helpers.js` wraps each native loader method and consults a `loaderOverrides` map installed by `setLoaderOverrides()`, with a `hasLoaderOverrides` boolean short-circuit when the map is empty.
- `internal/modules/run_main.js` deliberately calls loader methods through `loaderMethods` rather than the `modules` binding directly, precisely so a mounted VFS can override them and a worker entry point can live inside one.
- Unmounting must invalidate loader state: `cleanForVfsPrefix()` walks `Module._cache`, `Module._pathCache`, `statCache`, `realpathCache`, and the package.json caches, deleting entries under a mount prefix — by value for `_pathCache`, whose keys are `request\0parent` tokens.
- The provider selection pipeline is source-sniffing, not extension-based: `canHandle()` for `zip` attempts `ZipFile.openSync()` (which locates the end-of-central-directory record) and stashes the opened archive in `pendingZipFile` so `create()` does not reopen it; `dir` falls back to `stats.isDirectory()`.
- Permission denials feed a diagnostics channel: `Permission::is_scope_granted` calls `Publish()` on failure, which emits `{scope, resource, dropped}` on a per-scope channel and guards against recursion with a `publishing_` flag so a subscriber's own checks cannot re-enter.
- `normalizeReference()` in the JS permission layer canonicalises input across three representations — `URL` (converted with `fileURLToPath` only for `fs.*` scopes), `Uint8Array` (passed through raw so non-UTF-8 paths are not mangled), and validated strings.
Behavior traces
Three flows characterise the runtime: cold startup of a script, mounting a VFS and loading an entry point from inside it, and a permission-checked FFI `dlopen`. All three share the same spine — C++ picks a main script, that script calls `prepareExecution()`, and `prepareExecution()` decides what the realm is allowed to do. The VFS trace is the most intricate because mounting is deliberately deferred past `--import` preloads so a preload can register a custom provider. The FFI trace is the clearest example of a two-check security path, where a purely in-memory operation still demands filesystem-write permission for platform-consistency reasons.
- Cold start (script): `main()` → `node::Start` → `sea::FixupArgsForSEA` → `StartInternal` → `LoadSnapshotData` → `NodeMainInstance::Run` → C++ `StartExecution` picks `internal/main/run_main_module`.
- `run_main_module.js` then calls `prepareMainThreadExecution(!isEntryURL)` → `markBootstrapComplete()` → `RegExpPrototypeExec(/^/, '')` to reset RegExp statics before user code → `Module.runMain(mainEntry)`.
- `Module.runMain` is invoked even for ESM entry points solely to preserve legacy monkey-patchability of `Module.runMain` by `--require` preloads; the file comments this as undesirable and slated for removal once module customization hooks suffice.
- Inside `prepareExecution()` the order is fixed: `refreshRuntimeOptions()` → `patchProcessObject()` → trace/inspector/navigator/warning setup → the feature `setupX()` block → `initializeReport()` → `setupDiagnosticsChannel()` → `initializePermission()` → source maps → deprecations → `setupVmModules()` → `initializeModuleLoaders()` → `finishVfsMounts()` → `setupHttpProxy()`.
- Each flag-gated builtin is unlocked by the same three lines — check `getOptionValue('--experimental-X')`, require `BuiltinModule` from `internal/bootstrap/realm`, call `allowRequireByUsers('X')` — repeated verbatim in `setupBench`, `setupFFI`, `setupSQLite`, `setupStreamIter`, `setupDTLS`, `setupQuic`, `setupVfs`.
- VFS mount trace: `finishVfsMounts()` guards on `vfsMounted` → reads `--vfs-mount` entries → `path.resolve` + `fs.statSync` each (throwing `ERR_VFS_INVALID_TARGET` on failure or on a non-file/non-dir) → `selectProvider()` → `new VirtualFileSystem(provider)` → `vfs.mount()`.
- Mounting is called from two places with an idempotence guard: from `prepareExecution()` only when `--import` is empty, otherwise from `run_main` after the `--import` loop — so an `--import` preload can `registerProvider()` before any source's provider is chosen.
- For `--vfs-load`, `getVfsLoadIndex()` re-walks `process.execArgv`-style option names to recover which position in the shared `vfs_mounts` list came from `--vfs-load`, because the option parser stores plain strings and cannot record provenance.
- The `--vfs-load` source is spliced into `process.argv` at index 1 rather than overwriting it, so the program's own first positional argument is not silently dropped; `argv[1]` names the real source while the reserved mount point stays an implementation detail.
- `src/node.cc` routes to `run_main_module` when `vfs_load` is set even with no positional argument, instead of falling through to the REPL or stdin-eval — a dedicated branch added for this flag.
- FFI trace: `new ffi.DynamicLibrary(path)` → `DynamicLibrary::New` → construct-call check → `THROW_IF_INSUFFICIENT_PERMISSIONS(kFFI)` → null-byte check on the path → optional byte materialisation → `uv_dlopen` → `image.AfterOpen()`.
- When an FFI library is supplied as bytes (a library living inside a VFS that `dlopen` cannot reach by path), the code additionally demands `kFileSystemWrite` on `AddonImage::TempDir()` — and comments that the check is enforced even on Linux where an anonymous memfd is used, so grants do not vary by platform.
Testing signals
Testing is dominated by the bespoke Python harness `tools/test.py`, which discovers suites under `test/<suite>/`, applies per-suite `.status` files for SKIP/SLOW/FLAKY expectations, and shells out to the built `node` binary per test file. There is no Vitest/Jest; the stack tag is misleading. Native code is covered by `cctest` (googletest) and the `js-native-api`/`node-api`/`addons` suites, which require a build step before they can run. Coverage is collected by nyc/c8 with `.nycrc` thresholds of 95% lines/statements and 93% branches, uploaded to Codecov from two Linux workflows. The new subsystems are well covered at the JS level — ~121 `test-vfs-*` files in `test/parallel` alone plus a dedicated `test/ffi/` suite — but SEA-specific paths are explicitly excluded from coverage.
- The harness is `tools/test.py`, a Python 3 runner invoked as `./tools/test.py --flaky-tests keep_retrying -p actions -j 4`; suites are directories under `test/` and each may carry a `<section>.status` file read by `ReadConfigurationInto`.
- `test/root.status` encodes mode-dependent expectations (e.g. a long list of `parallel/test-*: SLOW` entries under `[$mode==debug]`), and `test/ffi/ffi.status` skips `test-ffi-calls`, `test-ffi-shared-buffer`, and `test-ffi-weakref-calls` on Solaris/SmartOS where bundled libffi callbacks crash.
- `test/parallel` is by far the largest suite (~4976 files), followed by `test/fixtures` (~6108 support files), `test/es-module` (246), `test/addons` (185), `test/sequential` (125), `test/module-hooks` (114), and `test/test-runner` (113).
- VFS has roughly 121 dedicated files in `test/parallel` covering per-API parity (`test-vfs-fs-mkdirSync`, `test-vfs-fs-copyFileSync`, `test-vfs-fs-createReadStream`), edge cases (`test-vfs-bigint-position`, `test-vfs-ctime-update`), and an explicit `test-vfs-fs-matches-real-fs` differential test.
- FFI has its own compiled suite `test/ffi/` with a `fixture_library`, a `testcfg.py`, and permission-focused tests: `test-ffi-permissions.js`, `test-ffi-permission-allow-cli.js`, `test-ffi-permission-audit.js`, `test-ffi-permission-drop.js`, plus `test-ffi-vfs.js` for the load-from-bytes path.
- Running the JS suites requires native build steps first: `make jstest` depends on `build-addons build-js-native-api-tests build-node-api-tests build-sqlite-tests build-ffi-tests` — running `tools/test.py` on a clean tree will fail those suites.
- `make cctest` runs the C++ googletest binary built from `test/cctest/` (37 files) and is a separate gate from the JS suites.
- CI suite composition is `CI_JS_SUITES = $(JS_SUITES) pummel` and `CI_NATIVE_SUITES = $(NATIVE_SUITES) benchmark`, with `doctool` conditionally appended; `make test-ci` runs all three plus the prerequisite builds.
- `.github/workflows/test-linux.yml` builds on ubuntu-24.04 and ubuntu-24.04-arm with clang-19, Python 3.14, Rust 1.88 and sccache, runs `make build-ci` then `make test-ci -j1 TEST_CI_ARGS="-p actions --measure-flakiness 9"`.
- CI asserts test hygiene two ways: a `git add -A && git diff --exit-code --staged` step fails the build if running tests dirtied the tree, and the whole suite is re-run from a directory named with spaces, quotes, and non-ASCII characters to catch path-handling bugs.
- Coverage thresholds are declared in `.nycrc` — 95% lines, 95% statements, 93% branches — over `lib/` only, with `test/`, `tools/`, `benchmark/`, and `deps/` excluded.
- Codecov is configured to wait for two coverage builds (`coverage-linux.yml` and `coverage-linux-without-intl.yml`; the Windows one is noted as manually disabled) and both project and patch status gates are turned off, so coverage is informational rather than blocking.
Risk map
The highest-value security surfaces are the permission model, `node:ffi`, and the VFS path router — and they interact. FFI is a documented memory-unsafe API whose only guard is the `kFFI` scope; the VFS is documented as explicitly *not* a security boundary despite superficially resembling a sandbox; and the permission model's correctness depends on non-obvious hardening (freezing `path`, killing `process.binding`) that a refactor could silently undo. Fragile coupling concentrates in the VFS's reliance on string-prefix path ownership and on cache invalidation across five separate module-loader caches. The `--vfs-load` provenance recovery by re-parsing execArgv is the most brittle single mechanism in the tree.
- `node:ffi` is memory-unsafe by design — the docs state that invalid pointers, wrong signatures, or use-after-free 'can crash the process or corrupt memory' — and its only gate is `THROW_IF_INSUFFICIENT_PERMISSIONS(kFFI)`, which is a no-op unless `--permission` is on.
- `--allow-ffi` is only offered in `availableFlags()` when `--experimental-ffi` is set, so the flag surface changes shape depending on another flag; a script probing `availableFlags()` for capability discovery can get inconsistent answers.
- The VFS is documented as NOT a sandbox: 'The VFS API is not a sandbox, permission system, or access-control mechanism', and `RealFSProvider`'s root-escape rejection is explicitly stated not to be a security boundary. Any code treating a mount as isolation is wrong.
- VFS path ownership is decided purely by string prefix against `${os.devNull}/vfs`; the safety argument rests on `os.devNull` being a character device that cannot have children. A platform where that assumption fails, or a `path.resolve` normalisation difference, would break routing exactness.
- `getVfsLoadIndex()` recovers which mount came from `--vfs-load` by re-walking argument names and counting `--vfs-mount`/`--vfs-load` occurrences, including manual handling of the space-separated `--vfs-mount value` form. Any change to option parsing or aliasing silently misattributes the entry point.
- Unmount correctness depends on `cleanForVfsPrefix()` clearing five distinct caches (`Module._cache`, `Module._pathCache` by value, `statCache`, `realpathCache`, package.json caches); missing one leaves resolvable stale entries pointing into an unmounted layer.
- The permission model's integrity relies on two side effects that look like incidental hardening: replacing `process.binding` with a thrower and `ObjectFreeze(require('path'))`. Removing either reopens a documented bypass.
- `Permission::Publish` guards re-entrancy with a mutable `publishing_` flag rather than a scope guard; an exception thrown from a diagnostics-channel subscriber would leave `publishing_` stuck true and silently suppress all further permission telemetry.
- Zip provider detection performs a trial `ZipFile.openSync()` inside `canHandle()` and caches the handle in module-level `pendingZipFile`; if a later provider's `canHandle` throws or `create()` is never reached, that file handle leaks and a subsequent probe reuses stale state.
- `registerProvider()` unshifts onto a module-global array with no removal API and no ordering control beyond 'newest wins', and the doc comment confirms a registered provider 'can back, wrap, or vet any source' — a `--require`/`--import` preload can therefore intercept every subsequent mount.
- `node --watch` shells out: it `spawn`s `process.execPath` with reconstructed argv and manipulates `NODE_OPTIONS`, making it a command-construction path where flag stripping bugs change what the child is allowed to do (a recent commit addressed exactly this: 'watch: strip watch flags from NODE_OPTIONS in child process').
- `src/node_file.cc` is the largest source file (5851 lines) and contains ~55 `THROW_IF_INSUFFICIENT_PERMISSIONS` call sites; each new fs syscall wrapper is an opportunity to omit one, and there is no compile-time enforcement that a binding method performs its check.
Extension points
This codebase is unusually explicit about where new code plugs in, because adding a feature is a repeated four-part ritual. New native capability goes in the `NODE_BUILTIN_*_BINDINGS` macro lists; new CLI surface goes in `src/node_options.cc` with a field in `node_options.h`; new run modes go in the `StartExecution` if-chain plus a file in `lib/internal/main/`; new user-visible builtins are gated by a `setupX()` in `pre_execution.js` calling `BuiltinModule.allowRequireByUsers()`. Beyond core, the VFS provider registry, the CJS `Module._extensions` table, the loader-override map, and the SQLite virtual-table module are genuine runtime plugin surfaces.
- `BuiltinModule.allowRequireByUsers(id)` is the runtime gate that makes a `lib/*.js` module requirable by user code; it adds to `canBeRequiredByUsersList` and conditionally to `canBeRequiredByUsersWithoutSchemeList` (new experimental modules are added `node:`-scheme-only).
- `vfs.registerProvider({ name, canHandle, create })` is the public plugin interface for `--vfs-mount` sources; registrations are unshifted ahead of the built-in `zip` and `dir` providers, so a custom provider outranks both.
- `VirtualProvider` in `lib/internal/vfs/provider.js` is the base class a custom provider subclasses; the shipped implementations (`MemoryProvider`, `RealFSProvider`, `ZipProvider`, and the internal SEA provider) are the reference shapes.
- `setLoaderOverrides(overrides)` installs per-key replacements for the module loader's fs and package.json operations; passing a nullish field restores the native fast path, which is how unmount cheaply reverts loader interception.
- `Module._extensions` is the classic CJS extension table (`.js`, `.json`, `.node` assigned in `cjs/loader.js`) and `findLongestRegisteredExtension` supports multi-dot extensions, so a loader plugin can register e.g. `.test.ts`.
- `Module._load`, `Module._resolveFilename`, and `Module._findPath` remain intentionally monkey-patchable — `resolveMainPath()` comments that `Module._findPath` 'is monkey-patchable here', and the repo's own `eslint.config.mjs` patches `Module._resolveFilename` to resolve vendored ESLint plugins.
- Adding a native binding means adding `V(name)` to the appropriate macro list in `src/node_binding.h` — `NODE_BUILTIN_STANDARD_BINDINGS` for unconditional ones, or a new `#if`-guarded `NODE_BUILTIN_X_BINDINGS(V)` block for optional subsystems.
- Adding a CLI flag is a paired edit: `AddOption("--flag", help, BOOL_FIELD(field), kAllowedInEnvvar-style)` in `src/node_options.cc` and the field declaration in `src/node_options.h`; `Implies()` links derived bools (as `--vfs-load` implies `[vfs_load_set]`).
- Adding an execution mode requires a branch in the `StartExecution` chain in `src/node.cc` and a corresponding script in `lib/internal/main/`; `bench_runner` is the most recent instance of this pattern and mirrors `test_runner` line for line.
- `sqlite.createModule()` backed by `VirtualTableModule` lets user JS supply `xBestIndex`/`xOpen`/`xClose` behaviour into SQLite's `sqlite3_module` vtable — a genuine C++-level plugin surface reachable from JS.
- Permission scopes are extended by adding a `V(Name, "dotted.name", Parent, "--allow-flag")` entry to the `PERMISSIONS(V)` macro in `permission_base.h`; the enum, the diagnostics channel names, and the string mapping are all generated from it.
- Test suites are pluggable: each `test/<suite>/` may carry a `testcfg.py` and a `<suite>.status` file that `tools/test.py` discovers, as `test/ffi/` does.
Contributor handoff
Read `AGENTS.md` first — it is binding policy, not advice, and it forbids an agent from pushing, opening PRs, or posting comments without active human oversight. Code style in `lib/` is not ordinary JavaScript: everything goes through `primordials` to be immune to user monkey-patching, and a custom ESLint plugin enforces it. Before editing, understand that `lib/` is snapshotted at build time, so top-level side effects in bootstrap files have outsized consequences. Nothing in `deps/` should be hand-edited. The realistic verification loop is a full build followed by targeted `tools/test.py` runs, not a fast unit-test cycle.
- `AGENTS.md` requires `Signed-off-by:` from the human contributor and an `Assisted-by: <agent name>` annotation, and explicitly prohibits `Signed-off-by:`/`Co-authored-by:` naming an agent, pushing to any branch or tag, and creating unsupervised PRs or issues.
- Read these first, in order: `AGENTS.md`, `CONTRIBUTING.md`, `doc/contributing/primordials.md`, `doc/contributing/cpp-style-guide.md`, and `lib/internal/bootstrap/node.js` (whose header comment is the best single explanation of startup ordering).
- Every file in `lib/` destructures from `primordials` at the top (`ArrayPrototypePush`, `StringPrototypeStartsWith`, `ObjectFreeze`, …) instead of using built-in methods directly; new code must match this or `tools/eslint-rules` will reject it.
- Never hand-edit `deps/`: `deps/crates/Cargo.toml` states 'This manifest is generated by node-core-utils. Do not modify it directly', and dependency bumps are driven by `tools/dep_updaters/` and scheduled workflows.
- Anything added to `lib/internal/bootstrap/` must be synchronous and must not read argv or env at top level, because it is executed at snapshot-build time; runtime-dependent setup belongs in `pre_execution.js` or a `lib/internal/main/` script.
- Verification loop: `./configure && make -j<n>` then `python3 tools/test.py parallel/test-<pattern>` for targeted runs; full validation is `make test-only` (build addons + cctest + jstest + tooltest without rebuilding docs).
- Lint before proposing a change: `make lint` runs js/cpp/md/yaml; `make lint-ci` adds Python lint and a merge-conflict-marker scan across `benchmark deps doc lib src test tools` that fails the build on stray `<<<<<<<`/`>>>>>>>`.
- Safe edit boundary: a feature that is JS-only (a new provider, a runner reporter, a stream helper) can be added without touching C++. Anything needing a new syscall, a new option, or a new permission scope requires coordinated edits across `src/node_options.{cc,h}`, `src/node_binding.h`, and `pre_execution.js`. (inferred)
- Trap: options are read twice — once at parse time into `EnvironmentOptions` and again in JS via `getOptionValue()` after `refreshRuntimeOptions()`. Caching an option value at JS module scope before that call yields a stale snapshot-time value.
- Trap: `permission.js` memoizes `_permission`, `_audit`, and `_ffi` on first read, so an option consulted before `refreshRuntimeOptions()` is frozen for the process lifetime.
- Trap: worker option state is not the main thread's — `src/node_worker.cc` overwrites `vfs_load` to false per worker, so a JS-side `getOptionValue('--vfs-load')` reasoning must account for thread identity.
- Trap: `test/ffi`, `test/sqlite`, `test/addons`, `test/js-native-api`, and `test/node-api` all need a build step (`make build-ffi-tests`, etc.) before `tools/test.py` will pass on them; failures there on a fresh tree usually mean 'not built', not 'broken'.
Languages
| Language | Files | Share of source |
|---|---|---|
| JavaScript | 23278 | 58% |
| C | 10589 | 26% |
| C++ | 4546 | 11% |
| HTML | 1519 | docs/config |
| Rust | 973 | 2% |
| JSON | 794 | docs/config |
| Markdown | 682 | docs/config |
| Python | 570 | 1% |
Start reading here
Probable source entrypoint: src/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
src/ (495 files) lib/ (429 files)
Repository structure
| Top-level path | Files |
|---|---|
test/ | 863 |
deps/ | 863 |
benchmark/ | 637 |
src/ | 495 |
lib/ | 429 |
tools/ | 391 |
doc/ | 162 |
.github/ | 60 |
Docs found: AGENTS.mdCONTRIBUTING.mdREADME.md
Package
pyproject.toml — , 0 declared dependencies.
CI & config entrypoints
README.md— Repository readmeAGENTS.md— Agent instructions.github/workflows/author-ready-conflicts.yml— GitHub Actions workflow.github/workflows/auto-start-ci.yml— GitHub Actions workflow.github/workflows/benchmark.yml— GitHub Actions workflow.github/workflows/build-shared.yml— GitHub Actions workflow.github/workflows/build-tarball.yml— GitHub Actions workflow.github/workflows/close-stalled.yml— GitHub Actions workflow.github/workflows/codeql.yml— GitHub Actions workflow.github/workflows/comment-labeled.yml— GitHub Actions workflow
Structural health signals
Average score 5.4/10 across 96 analyzed source files; 94 hotspots surfaced.
lib/internal/webstreams/adapters.js— 1173 lines, score 4.5 (Large file, Deep nesting)deps/npm/test/lib/commands/install.js— 1166 lines, score 4.5 (Large file, Deep nesting)lib/internal/webstreams/readablestream.js— 1662 lines, score 4.7 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
976a36a5b6ce2026-09-20 — 2026-09-22, Version 26.10.0 (Current)d4120cbcc6172026-09-22 — tools: update nixpkgs-unstable to 79b35bf0bda5cd110f856aa5b5b2c5ba44644ff2db1e6712026-09-22 — typings: share AsyncWrap and HandleWrap types1eacebb143082026-09-22 — watch: strip watch flags from NODE_OPTIONS in child processdd9f4dac32962026-09-21 — sqlite: add virtual table support via createModule()