Open-Source Wikis

/

Node.js

/

Systems

/

Module loaders

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-exports module.exports plus named exports detected by cjs-module-lexer (vendored at deps/cjs-module-lexer/).
  • 'json' — wraps a JSON parse as a synthetic module exposing a default.
  • 'builtin' — wraps a built-in module.
  • 'wasm' — uses WebAssembly.compile and links the Wasm module exports.
  • 'addon' (experimental) — .node addons 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-loader legacy or auto): hooks run in a worker; the main loader proxies via MessageChannel. Implementation: lib/internal/modules/esm/hooks.js and lib/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.js registers BuiltinModule for require('node:foo'). Modules whose names start with internal/ are not exposed to user code unless --expose-internals is on.
  • process.binding: Limited to a curated allow-list; modern code uses internalBinding.
  • Source maps: lib/internal/source_map/ integrates with both loaders to produce mapped stack traces.
  • Inspector: ESM import.meta.url and the V8 inspector's Debugger.getScriptSource both round-trip through ModuleWrap.

Entry points for modification

To add a built-in module:

  1. Add lib/<name>.js (public re-export shell) and lib/internal/<name>.js or lib/internal/<name>/ (real code).
  2. Register the file in node.gyp's library_files.
  3. Add it to lib/internal/bootstrap/realm.js's allow-list if it should be reachable through process.binding (rarely needed).
  4. 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.

Module loaders – Node.js wiki | Factory