Skip to content

hooks: in-thread ESM hooks from Node.js 22.22.3, 24.11.1 and 25.1 (nodejs/node#59929) - #53

Merged
toabctl merged 1 commit into
mainfrom
wt-node-sync-hooks
Oct 14, 2026
Merged

toabctl merged 1 commit into
mainfrom
wt-node-sync-hooks

Conversation

@toabctl

@toabctl toabctl commented Oct 13, 2026

Copy link
Copy Markdown
Owner

The in-thread ESM hooks (module.registerHooks) were only used from Node.js 24.12 / 25.2, for a crash we had attributed to nodejs/node#60380. Plan 03's spikes found the real causes:

  • The crash (this[#customizations].loadSync is not a function): a --require preload calling registerHooks inside Node's own loader thread, which Node 22 – 24.11 runs preloads in too. register.cjs returns there, so our hooks never trigger it.
  • What really breaks in-thread hooks next to another loader (Yarn Plug'n'Play, tsx, OpenTelemetry's hook.mjs): a loader-thread hook that returns no source for a CommonJS module is rejected with ERR_INVALID_RETURN_PROPERTY_VALUE before module: handle null source from async loader hooks in sync hooks nodejs/node#59929, i.e. before Node 22.22.3, 24.11.1 and 25.1 — the same gate import-in-the-middle and dd-trace use. Verified: forced sync with OTel, dd-trace and PnP gives the right lockfiles on 22.22.3, 22.23.2 and 24.11.1, and fails on 22.22.2 and 24.10.0.

So from those versions on, the in-thread hooks are used in every process. Current Node 22 no longer needs the loader thread, which removes its cost and the bundler-process gate, the gate that misses pnpm/bun isolated layouts (plan 02 S1).

Changes

  • syncHooksSafe and the matrix's SYNC_HOOKS use those versions. A unit-tested version table covers 22.22.2, 24.10.0, 24.11.0 and 25.0, which stay on the loader thread.
  • CI: the Node 22 matrix job also runs on 22.22.2 (nodejs-22=22.22.2-r1), the last 22 with the loader-thread hooks that older Node.js uses. Latest 22 now covers the in-thread path.
  • When module.register fails, the error names what breaks Node's loader thread (another tool's loader, or a preload that calls registerHooks) instead of blaming bundle-lockfile.
  • New unit tests:
    • the real entry module sees its own file through import.meta, despite the ?bundle-lockfile-real URL;
    • a cycle back into a wrapped entry reads the wrapper. This is documented in internals.md; none of the wrapped entries has such a cycle.
  • The punycode test no longer assumes every Node 22 stays silent for a package's require(): 22.22.2 warns there, with and without bundle-lockfile.
  • Docs: README, bundlers.md, internals.md and troubleshooting.md give the versions and the real causes.

Verified on the workstation (Wolfi containers)

  • Full matrix on Node 22.23.2, now in-thread: 413/413.
  • Node 22 CI subset on 22.22.2 (loader thread), 22.22.3 and 24.11.1: 241/241 each.
  • Unit tests on 20.20.2, 22.22.2, 22.22.3, 22.23.2, 24.10.0, 24.11.1, 24.21.0 and 26.11.1: all pass.

🤖 Generated with Claude Code

@toabctl
toabctl force-pushed the wt-node-sync-hooks branch 4 times, most recently from 8828cd1 to b7ba289 Compare October 14, 2026 00:59
…dejs/node#59929)

The in-thread hooks (module.registerHooks) were used only from Node.js 24.12 / 25.2, for a crash attributed to
nodejs/node#60380: "registerHooks next to a loader-thread hook crashes". Spikes show the crash ("this[#customizations]
.loadSync is not a function") comes from a --require preload calling registerHooks inside Node's own loader thread,
which Node 22 - 24.11 run preloads in too; register.cjs does nothing there, so our hooks never trigger it. What does
break in-thread hooks next to another loader is a loader-thread hook that returns no source for a CommonJS module
(Node reads the file): rejected with ERR_INVALID_RETURN_PROPERTY_VALUE before nodejs/node#59929 - Node 22.22.3, 24.11.1,
25.1 (the versions import-in-the-middle and dd-trace check). Forced sync mode with OpenTelemetry's hook.mjs, dd-trace and
Yarn Plug'n'Play: all builds right on 22.22.3, 22.23.2, 24.11.1; every one with another loader fails on 22.22.2 and
24.10.0.

So the in-thread hooks are used from those versions on, in every process: current Node.js 22 no longer needs the
loader thread (its cost, and the bundler-process gate that misses pnpm's and bun's isolated layouts).

- syncHooksSafe, and the test matrix's SYNC_HOOKS, by those versions; unit test of the version table (22.22.2,
  24.10.0, 24.11.0, 25.0: loader thread)
- CI: the Node 22 matrix job also on 22.22.2 (nodejs-22~22.22.2), the last 22 with the loader-thread hooks,
  which older Node.js uses
- the message when module.register fails names what breaks Node's loader thread (another tool's loader or a
  preload that calls module.registerHooks) instead of blaming bundle-lockfile
- unit tests: the real entry module sees its file through import.meta (the ?bundle-lockfile-real URL); a cycle back
  into a wrapped entry reads the wrapper (TDZ during evaluation, the wrapped function later), documented in
  internals.md
- test/node-versions.json: 22.22.2 / 24.10.0 the last with loader-thread hooks, 22.22.3 / 24.11.1 (pr) the first
  with in-thread ones; the safety net's messages and docs name the new versions
- docs: README, bundlers.md, internals.md, troubleshooting.md: the versions; internals.md: the real causes

Verified on the workstation: the full matrix on Wolfi's Node 22 (22.23.2, now in-thread), the Node 22 subset on 22.22.2
(loader thread), 22.22.3 and 24.11.1, unit tests on 20, 22.22.2, 22.22.3, 22.23.2, 24.10.0, 24.11.1, 24, 26.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@toabctl
toabctl force-pushed the wt-node-sync-hooks branch from b7ba289 to ec22074 Compare October 14, 2026 01:31
toabctl added a commit that referenced this pull request Oct 14, 2026
CI failed twice tonight in fixture generation, before any test ran: yarn install got ECONNRESET from the registry
(main at dc21e2f, PR #53, both wp5-yarn4-pnpm). gen.cjs now runs an install again, up to 3 times with a short pause,
when its output names a network error (ECONNRESET, ETIMEDOUT, EAI_AGAIN, ECONNREFUSED, EPIPE, socket hang up); any
other failure fails at once, as before. Only fixture creation retries; no assertion changes.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@toabctl
toabctl merged commit 9b3e1c3 into main Oct 14, 2026
52 checks passed
@toabctl
toabctl deleted the wt-node-sync-hooks branch October 15, 2026 20:19
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