Skip to content

feat: use module.registerHooks() instead of deprecated module.register() - #19

Merged
Julien-R44 merged 2 commits into
fix/import-meta-hot-injectionfrom
feat/sync-module-hooks
Nov 6, 2026
Merged

Julien-R44 merged 2 commits into
fix/import-meta-hot-injectionfrom
feat/sync-module-hooks

Conversation

@Julien-R44

@Julien-R44 Julien-R44 commented Nov 6, 2026 •

Copy link
Copy Markdown
Owner

Closes #18

Why

Node.js 26 emits a runtime deprecation for module.register() (DEP0205), and it will be removed in a future version. hot.init() called it, so every app using hot-hook printed the warning on Node 26.

What changes

  • hot.init() registers synchronous, in-thread hooks with module.registerHooks() on Node.js >= 22.22.3, 24.11.1, 25.1.0 and 26.
  • Older versions fall back to module.register(). registerHooks() exists since 22.15, but before those versions a sync load hook crashes on CommonJS (ERR_INVALID_RETURN_PROPERTY_VALUE) as soon as any module.register() hook is also registered (ts-node, older tsx, @poppinss/ts-exec triggers it too). The thresholds were bisected on official binaries and match tsx 4.23's own.
  • The same HotHookLoader serves both modes. resolve/load are no longer async; a small then() helper (maybe_async.ts) stays synchronous when nextResolve/nextLoad return plain objects and chains otherwise. DynamicImportChecker is now sync (readFileSync + parseImportsSync).
  • With in-thread hooks, the chokidar watcher runs in a dedicated worker (watcher_thread.ts). On the main thread it kept the process alive and missed changes made right after startup. The worker is unref'd after its initial scan, hot.init() waits for that scan, and worker errors or an early exit can't block init.
  • load copies context instead of deleting importAttributes.hot (frozen in-thread), and decodes source explicitly: with an async loader in the chain it is a Uint8Array, and toString() produced comma-separated bytes, so the entrypoint silently did nothing.
  • CI also runs Node 26, and 22.15 to cover the fallback.

Behaviour change: with in-thread hooks, require() calls also go through hot-hook (noted in the changeset).

Verification

Note

Stacked on #20, which fixes the import.meta.hot injection regression from ed28a8c (AdonisJS controllers stopped hot reloading). Merge #20 first. With in-thread hooks, hot-hook wraps the TS loader's output, so the inline source map shift from #20 also applies to @poppinss/ts-exec (Adonis 7): line-1 positions with --enable-source-maps are identical with and without hot-hook on Node 26.

  • pnpm quick:test: 31/31 (with fix: inject import.meta.hot into every module using import.meta #20) on Node 22.14, 22.15, 22.22.3, 24.11.1, 24.22.2 and 26.12.1. New tests: the process exits without DEP0205 (fails on main), and HMR works after a module.register() loader (fails without the source decoding fix).
  • QA in fresh create-adonisjs apps with this build installed, driving node ace serve --hmr: boot, HMR on a controller, on a service it imports and a second edit, runtime error then recovery, full reload on start/routes.ts, restart on .env, HMR after restart.
    • Adonis 6 (ts-node-maintained): 9/9 on Node 22.14, 22.15, 22.22.3, 24.11.0, 24.22.2, 26.12.1 and 26.13.0. The remaining DEP0205 on 26 comes from ts-node-maintained.
    • Adonis 7 (ts-exec): 9/9 on Node 24.11.1, 24.22.2, 26.12.1 and 26.13.0, no DEP0205. On 24.11.0 it crashes at boot exactly like the published hot-hook (Node bug triggered by ts-exec).
    • Run with this branch on top of fix: inject import.meta.hot into every module using import.meta #20, installed from a packed tarball.
  • Perf vs published 1.0.0 (median of 5, 1000 sequential requests):
    • Adonis 7 on Node 26: boot 956 → 662 ms, 213 → 510 req/s, RSS 290 → 218 MB (no more hooks thread).
    • Adonis 6 on Node 26: boot 996 → 1098 ms, 288 → 278 req/s (ts-node keeps the hooks thread; ~40 ms and ~10 MB are the watcher worker).
  • tsx 4.21, tsx 4.23 and ts-node chains checked on 22.14 to 26.

Not tested: Windows and macOS (CI covers Windows).

Node.js 26 emits a runtime deprecation (DEP0205) for module.register().
hot.init() now registers synchronous in-thread hooks with
module.registerHooks() on Node.js >= 22.22.3, 24.11.1, 25.1.0 and 26,
and falls back to module.register() on older versions, whose
registerHooks() breaks CommonJS loading when combined with
module.register() hooks such as ts-node's.

The same HotHookLoader serves both modes. With in-thread hooks, the
chokidar watcher runs in a dedicated worker so it neither keeps the
process alive nor misses changes during startup.

Closes #18

Amp-Thread-ID: https://ampcode.com/threads/T-01a1b212-dc0a-7380-bad5-624903b77b96
c8 10 depends on yargs 17.7.2, which fails to load on Node.js 26
("require is not defined in ES module scope").

Amp-Thread-ID: https://ampcode.com/threads/T-01a1b212-dc0a-7380-bad5-624903b77b96
@Julien-R44
Julien-R44 force-pushed the feat/sync-module-hooks branch from dffc8e6 to 4aa8428 Compare November 6, 2026 21:34
@Julien-R44
Julien-R44 changed the base branch from main to fix/import-meta-hot-injection November 6, 2026 21:34
@Julien-R44
Julien-R44 merged commit 4aa8428 into fix/import-meta-hot-injection Nov 6, 2026
30 checks passed
@Julien-R44
Julien-R44 deleted the feat/sync-module-hooks branch November 6, 2026 21:59
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.

Node 26 deprecates module.register (DEP0205)

1 participant