Skip to content

node:module: implement synchronous module.registerHooks() (+49 tests) - #35690

Draft
cirospaciari wants to merge 12 commits into
claude/node-v26-fix-tlsfrom
claude/node-esm-loader-hooks
Draft

cirospaciari wants to merge 12 commits into
claude/node-v26-fix-tlsfrom
claude/node-esm-loader-hooks

Conversation

@cirospaciari

Copy link
Copy Markdown
Member

Implements the synchronous module customization hooks API — module.registerHooks() (Node 23+, the supported successor to module.register()) — and vendors the upstream test/module-hooks/ suite that exercises it.

What this does

module.registerHooks({ resolve, load }) now works in Bun for both require() and import:

  • resolve chain: hooks observe every user-visible resolution (require, require.resolve, static/dynamic import, import.meta.resolve), with Node's context contract (parentURL, conditions, importAttributes), nextResolve chaining, the shortCircuit requirement, and ERR_INVALID_RETURN_PROPERTY_VALUE validation.
  • load chain: hooks observe module loads with Node's {url, context.format} contract and can replace the source; returned format (module, commonjs, json, the *-typescript variants) maps onto Bun's loader + module-type pipeline. Builtins are observed with node:-prefixed URLs and a null default source (source overrides for builtins are ignored, like Node ignores them for builtin format).
  • hook.deregister(), hook chaining/merged contexts, and virtual specifiers (e.g. virtual:x ids produced by hooks) work.
  • module.register() stays a no-op but now emits Node's DEP0205 deprecation warning pointing at registerHooks().

How it's wired

  • src/js/internal/modules/customization_hooks.ts — port of Node's lib/internal/modules/customization_hooks.js chain/validation logic, plus the two Bun-facing entry points (runResolveHooksBun, runLoadHooksBun).
  • The native resolver funnel (VirtualMachine::resolve_maybe_needs_trailing_slash, and its jsc_hooks.rs twin) consults the JS resolve chain before the builtin-alias fast path; the module loader (transpile_file) consults the load chain before reading a module off disk; fetch_builtin_module consults it for builtins. All gated on plain integer counters mirrored onto VirtualMachine, so the hook-free hot path only pays an integer compare.
  • The chain's default steps call back into Bun's native resolution (with a reentrancy flag so the default step can't recurse into hooks) and fs.readFileSync.
  • New error codes ERR_INVALID_RETURN_PROPERTY_VALUE and ERR_UNKNOWN_MODULE_FORMAT (message builders match Node's text).

Tests

Vendored byte-verbatim from Node v26.3.0 (each verified failing on system Bun and passing on this build):

  • test/js/node/test/module-hooks/ — 47 of the 63 sync-hooks tests (the suite is added to the CI runner's inclusion list; a canary run verified the runner executes the new directory).
  • test/js/node/test/es-module/ — test-esm-import-meta-resolve-hooks.mjs and test-import-preload-require-cycle.js (the runner line matches the es-module suite PR; trivial merge overlap).

Not covered (dropped, with reasons)

  • Off-thread module.register() protocol — the 20 test-esm-loader-* failures in the es-module suite all depend on it (--experimental-loader/--loader children or direct register() calls); register() is deprecated upstream (DEP0205). Tests: gated on that feature.
  • custom-conditions* (3) — per-resolution context.conditions overrides need conditions plumbed through the native resolver per-call.
  • load-builtin-override-{commonjs,json,module} (3) — replacing a builtin's implementation via load-hook format override is unsupported.
  • *-inline-typescript* + preload (5) — depend on the 11 MB fixtures/snapshot/typescript.js upstream fixture.
  • load-async-and-sync, require-esm (2) — need off-thread register().
  • builtin-require, load-builtin-require (2) — require node:sea, which Bun does not implement.
  • create-require-with-url (1) — needs URL-string specifiers preserved through createRequire(url).
  • test-esm-import-attributes-identity.mjs (1) — needs the ES module map keyed by import attributes.
  • test-esm-register-deprecation.mjs (1) — the DEP0205 warning is implemented (with Bun-native tests in test/js/node/module/node-module-module.test.js), but the upstream test asserts Node's [DEP0205] stderr format and --throw-deprecation exit behavior, which Bun's warning printer does not produce.

…on hooks [allow size]

Adds Node's synchronous module customization hooks API (module.registerHooks,
Node 23+, the supported successor to the deprecated off-thread
module.register). Resolve and load hook chains run for require(),
require.resolve(), static and dynamic import, and import.meta.resolve, with
Node's contract: parentURL/conditions/importAttributes context, nextResolve/
nextLoad chaining, the shortCircuit requirement, format overrides
(module/commonjs/json/typescript variants), builtin observation with node:
URLs and null default source, deregister(), and
ERR_INVALID_RETURN_PROPERTY_VALUE / ERR_UNKNOWN_MODULE_FORMAT validation.

The chain and validation logic is a port of Node's
lib/internal/modules/customization_hooks.js into a new internal module.
The native side consults it from the resolver funnel
(resolveMaybeNeedsTrailingSlash and its jsc_hooks twin, before the builtin
alias fast path), from the module loader (transpileFile, before reading a
module off disk), and from fetchBuiltinModule for builtins - all gated on
hook counters mirrored onto VirtualMachine so the hook-free path only pays
an integer compare. The chain's default resolve step re-enters Bun's native
resolution under a reentrancy flag so it cannot recurse into the hooks.

module.register() stays a no-op but now emits Node's DEP0205 deprecation
warning (once per process, suppressed by --no-deprecation) pointing at
registerHooks(); covered by tests in node-module-module.test.js.
Vendors byte-verbatim from Node v26.3.0:

- test/js/node/test/module-hooks/: 47 of the 63 synchronous
  module.registerHooks() tests (plus their fixtures), all passing. The suite
  directory is added to the node-test runner's inclusion list; a deliberately
  failing canary run confirmed the runner executes the new directory.
- test/js/node/test/es-module/: test-esm-import-meta-resolve-hooks.mjs and
  test-import-preload-require-cycle.js (with the import-require-cycle
  fixtures). The es-module runner line matches the pending es-module suite
  branch, so the two merge cleanly.

Not vendored, with reasons: custom-conditions* (3) need per-resolution
conditions plumbed into the native resolver; load-builtin-override-* (3)
replace a builtin's implementation via load-hook format override, which is
unsupported; *inline-typescript* and preload (5) depend on the 11 MB
fixtures/snapshot/typescript.js fixture; load-async-and-sync and require-esm
(2) need the off-thread module.register() protocol; builtin-require and
load-builtin-require (2) require node:sea; create-require-with-url (1) needs
URL-string specifiers preserved through createRequire(url);
test-esm-import-attributes-identity.mjs needs the module map keyed by import
attributes; test-esm-register-deprecation.mjs asserts Node's [DEP0205]
stderr format and --throw-deprecation exit behavior, which Bun's warning
printer does not produce.
@robobun

robobun commented Jul 25, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 11:05 PM PT - Aug 21st, 2026

@robobun, your commit 0ae6e5f is building: #103292

@github-actions

Copy link
Copy Markdown
Contributor

Found 4 issues this PR may fix:

  1. Bun does not support module.registerHooks #27369 - Bun does not support module.registerHooks
  2. node:module is missing the named ESM export registerHooks #34171 - node:module is missing the named ESM export registerHooks
  3. node:module.register does not exist. #11905 - node:module.register does not exist
  4. Bun throws TypeError when using createImportFresh from import-fresh #31472 - Bun throws TypeError when using createImportFresh from import-fresh

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #27369
Fixes #34171
Fixes #11905
Fixes #31472

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Add module.registerHooks named export to node:module #34174 - Also implements module.registerHooks() (the synchronous module customization hooks API from Node.js)

🤖 Generated with Claude Code

robobun added 5 commits August 3, 2026 20:47
…aude/node-esm-loader-hooks

# Conflicts:
#	src/jsc/bindings/ErrorCode.ts
… fallout

- Restore src/jsc/bindings/BunHeapProfiler.h (deleted by #36500 on main,
  but still needed by GeneratedJS2Native.h for the $newCppFunction calls
  added on this branch).
- VirtualMachine.rs / jsc_hooks.rs: resolve_maybe_needs_trailing_slash
  now takes ResolveMode, not is_esm/is_user_require_resolve; derive the
  two bools from `mode` for Bun__runModuleResolveHooks. Use `&raw const`
  for the pointer args.
- web_worker.rs: parent_ref binding was lost in the merge; read
  heap_profiler_config via `(*parent)` like the neighbouring fields.
- permission.rs: switch to bun_threading::RwLock (no poisoning) and
  bun_core::env_var::NODE_OPTIONS per clippy disallowed-types/methods.
- clap Diagnostic fields pub (read by bun_runtime::cli::Arguments).
- path.rs resolve_posix_t pub(crate) (called from permission.rs).
- Minor clippy: then_some, contains(), SAFETY comment placement,
  unreachable_pub on exec_check.
…aude/node-esm-loader-hooks

# Conflicts:
#	src/jsc/bindings/BunHeapProfiler.h
#	src/runtime/cli/run_command.rs
#	src/runtime/permission.rs
@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Cross-reference: this resolves #27369 ("Bun does not support module.registerHooks"; #31472 and #34171 were already closed as duplicates of it), so a Fixes #27369 line in the description would close it on merge. The export-only stub in #34174 has been closed in favor of this PR.

One small thing the stub had that this does not: the node:module entry in docs/runtime/nodejs-compat.mdx still says module.register is not implemented, so it could be updated here to mention registerHooks.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants