nodejs/node
Module loaders
Owners: @nodejs/loaders.
Purpose
Implement the two module systems that user code uses — CommonJS and ECMAScript Modules — plus the resolver, the customization-hooks pipeline, the package.json reader, and the experimental TypeScript stripper. The loader sits between the V8 module record / Module class and the file system / network / data: URL sources of code.
Directory layout
lib/
module.js // public CJS Module re-export
internal/modules/
cjs/ // CommonJS
loader.js // Module class + require resolution
esm/ // ECMAScript Modules
loader.js // in-thread default loader
hooks.js // user customization hooks
worker.js // hooks-in-worker bridge
resolve.js // import specifier resolution
load.js // fetch source + format detection
get_format.js // format inference
translators.js // wrap CJS/JSON/wasm/builtin as ESM
module_job.js // graph dependency state machine
module_map.js // graph cache
assert.js // import attribute checks
shared_constants.js
utils.js // small ESM helpers
create_dynamic_module.js // `module` namespace fabrication
customization_hooks.js // module.register dispatcher
helpers.js // shared resolver helpers
package_json_reader.js // package.json parsing/cache
run_main.js // run_main_module.js helpers
typescript.js // amaro-based TS stripping
src/
module_wrap.{cc,h} // V8 ModuleWrap binding (ESM)
deps/
cjs-module-lexer/ // detect ESM-named-export CJS interop
amaro/ // SWC TS stripper (Rust)Key abstractions
| Type / file | Role |
|---|---|
Module (lib/internal/modules/cjs/loader.js) |
CommonJS module class. Holds exports, _load, _compile, _resolveFilename. |
Loader (lib/internal/modules/esm/loader.js) |
Default ESM loader; orchestrates resolve→load→link→evaluate. |
Hooks (lib/internal/modules/esm/hooks.js) |
Collects user-supplied resolve/load hooks via module.register. |
ModuleJob (lib/internal/modules/esm/module_job.js) |
Tracks the link state of a single ESM record. |
ModuleMap (lib/internal/modules/esm/module_map.js) |
Cache of (url, attributes) → ModuleJob. |
ModuleWrap (src/module_wrap.cc) |
V8 module wrapper exposed to JS (synthetic + source modules). |
packageJsonReader (lib/internal/modules/package_json_reader.js) |
Walks up the tree caching package.json lookups. |
customization_hooks.js |
The module.register(specifier, parentURL) API. |
lib/internal/modules/typescript.js |
Wraps amaro to strip types when --experimental-strip-types is on. |
How CommonJS resolution works
graph TD
Req[require id] --> Resolve[Module._resolveFilename]
Resolve --> Cache{cache hit?}
Cache -- yes --> Cached[return cached exports]
Cache -- no --> NodeMods[node_modules + package exports]
NodeMods --> Compile[Module._compile from source]
Compile --> Wrap["Module.wrap → (function(exports, require, module, __filename, __dirname){ ... })"]
Wrap --> Run[Run in current context]
Run --> Cache2[Module._cache]
Cache2 --> Return[exports]The resolver supports built-ins (fs, http, …), the node_modules walk, package exports and imports fields, conditions (require, import, node, default, …), and --experimental-import-meta-resolve. Built-ins take precedence over user-named modules unless prefixed (node:fs is unambiguous).
CJS is also able to require ESM modules synchronously since Node 22 (require(esm)), with the limitation that the ESM module must be top-level-await free. The implementation lives in lib/internal/modules/cjs/loader.js (requireESM) and bridges through lib/internal/modules/esm/translators.js.
How ESM linking works
graph LR
Import[import x from 'a'] --> Resolve[hooks.resolve chain]
Resolve --> URL[final URL + format]
URL --> Load[hooks.load chain]
Load --> Source[source + format]
Source --> Translate[translators -> ModuleSource / SyntheticModule]
Translate --> ModuleWrap[ModuleWrap]
ModuleWrap --> Link[Link dependencies recursively]
Link --> Evaluate[V8 module evaluate]
Evaluate --> Bindings[Live bindings exposed]The default chain hooks run in the main thread. module.register adds user hooks; if the user wants their hooks isolated from the main thread, the loader spawns lib/internal/modules/esm/worker.js and proxies hook calls over a MessagePort.
ESM-side translators in lib/internal/modules/esm/translators.js cover:
'commonjs'— produced by detecting CJS, returning a synthetic ESM that re-exportsmodule.exportsplus named exports detected bycjs-module-lexer(vendored atdeps/cjs-module-lexer/).'json'— wraps a JSON parse as a synthetic module exposing adefault.'builtin'— wraps a built-in module.'wasm'— usesWebAssembly.compileand links the Wasm module exports.'addon'(experimental) —.nodeaddons treated as ESM modules.
Customization hooks
module.register('./hooks.mjs', import.meta.url) registers user hooks. Two delivery modes:
- In-thread (default): hooks run on the main thread. Hooks are async; the loader awaits each.
- Worker (
--experimental-loaderlegacy or auto): hooks run in a worker; the main loader proxies viaMessageChannel. Implementation:lib/internal/modules/esm/hooks.jsandlib/internal/modules/esm/worker.js.
Hook contract (resolve/load) is documented at doc/api/module.md. The new --import and --require flags use the same plumbing.
TypeScript stripping (experimental)
--experimental-strip-types (and --experimental-transform-types) feed .ts/.mts/.cts files through amaro (Rust SWC binding) to remove type annotations before passing to V8. JS glue is at lib/internal/modules/typescript.js; the addon binary ships from deps/amaro/. The owners are @nodejs/typescript.
Integration points
- Bootstrap:
lib/internal/bootstrap/realm.jsregistersBuiltinModuleforrequire('node:foo'). Modules whose names start withinternal/are not exposed to user code unless--expose-internalsis on. process.binding: Limited to a curated allow-list; modern code usesinternalBinding.- Source maps:
lib/internal/source_map/integrates with both loaders to produce mapped stack traces. - Inspector: ESM
import.meta.urland the V8 inspector'sDebugger.getScriptSourceboth round-trip throughModuleWrap.
Entry points for modification
To add a built-in module:
- Add
lib/<name>.js(public re-export shell) andlib/internal/<name>.jsorlib/internal/<name>/(real code). - Register the file in
node.gyp'slibrary_files. - Add it to
lib/internal/bootstrap/realm.js's allow-list if it should be reachable throughprocess.binding(rarely needed). - Document at
doc/api/<name>.md.
To change ESM resolution: most changes go in lib/internal/modules/esm/resolve.js; cross-check with the WHATWG resolver semantics in doc/api/esm.md.
To add a translator for a new file type: extend lib/internal/modules/esm/translators.js and get_format.js, and ensure CJS require(esm) interop still works.
For end-to-end tests: test/es-module/, test/parallel/test-module-*.js, test/parallel/test-require-*.js, test/module-hooks/.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.