Skip to content

Register ESM hooks with registerHooks() on Node 26+ - #121

Draft
rosston wants to merge 1 commit into
mainfrom
node-26-register-hooks
Draft

rosston wants to merge 1 commit into
mainfrom
node-26-register-hooks

Conversation

@rosston

@rosston rosston commented Sep 22, 2026

Copy link
Copy Markdown
Member

module.register() is deprecated as of Node 26 and warns once per process that replaces an ES module without --loader. Use module.registerHooks() (synchronous, in-thread) instead when it's safe to:

  • registerHooks() also intercepts CommonJS require(), which register() never did, so the sync resolve hook skips resolutions whose context.conditions includes "require".
  • registerHooks() exists on Node 22.15+/23.5+, but a Node bug (fixed in 22.23, 24.12, 25.2 and 26.0) means a sync hook can't be combined with an off-thread loader (--loader, tsx, ts-node, ...) once ESM imports CommonJS. So registerHooks() is only used where register() doesn't exist at all, or on Node 26+.
  • canRegisterLoader() now returns true if either API exists.

Extracted the plan/resolve/load decision logic shared by the existing async hooks (quibble.mjs) and the new sync hooks (quibble-sync-hooks.js) into its own module.

What's left un-shared, because it's genuinely specific to one style of hook:

  • the sync hooks' skip of CommonJS require() (registerHooks() sees require(), Module.register() never did)
  • the sync hooks' stripping of a __quibble query tag a coexisting --loader=quibble may have already added (finishResolve's normalizeUrl parameter)

Adds test/esm-lib/quibble-loader-registration.test.js, which spawns fresh Node processes (so quibble has to auto-register) to check stubbing, CJS require() is untouched, no DEP0205 on stderr, and coexistence with another off-thread loader.

module.register() is deprecated as of Node 26 and warns once per process
that replaces an ES module without --loader. Use module.registerHooks()
(synchronous, in-thread) instead when it's safe to:

- registerHooks() also intercepts CommonJS require(), which register()
  never did, so the sync resolve hook skips resolutions whose
  context.conditions includes "require".
- registerHooks() exists on Node 22.15+/23.5+, but a Node bug
  (fixed in 22.23, 24.12, 25.2 and 26.0) means a sync hook can't be
  combined with an off-thread loader (--loader, tsx, ts-node, ...) once
  ESM imports CommonJS. So registerHooks() is only used where register()
  doesn't exist at all, or on Node 26+.
- canRegisterLoader() now returns true if either API exists.

Extracted the plan/resolve/load decision logic shared by the existing
async hooks (quibble.mjs) and the new sync hooks (quibble-sync-hooks.js)
into its own module.

What's left un-shared, because it's genuinely specific to one style of
hook:
- the sync hooks' skip of CommonJS require() (registerHooks() sees
  require(), Module.register() never did)
- the sync hooks' stripping of a __quibble query tag a coexisting
  --loader=quibble may have already added (finishResolve's
  normalizeUrl parameter)

Adds test/esm-lib/quibble-loader-registration.test.js, which spawns
fresh Node processes (so quibble has to auto-register) to check
stubbing, CJS require() is untouched, no DEP0205 on stderr, and
coexistence with another off-thread loader.
@rosston rosston self-assigned this Sep 22, 2026
@rosston rosston changed the title Register ESM hooks with registerHooks() on Node 26+ to drop DEP0205 Register ESM hooks with registerHooks() on Node 26+ Sep 22, 2026
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.

1 participant