next.js
014b9987 - Turbopack: implement module.hot.accept(deps, cb) and module.hot.decline(deps) (#90443)

Commit
51 days ago
Turbopack: implement module.hot.accept(deps, cb) and module.hot.decline(deps) (#90443) ### What? Implements dependency-level HMR accept and decline for Turbopack, covering both ESM (`import.meta.turbopackHot`) and CJS (`module.hot`) modules. Previously Turbopack only supported self-accept (`module.hot.accept()` with no arguments) and self-decline. This PR adds the full dependency-targeted API: - `module.hot.accept(dep, cb)` / `import.meta.turbopackHot.accept(dep, cb)` — single dep - `module.hot.accept([depA, depB], cb)` / `import.meta.turbopackHot.accept([depA, depB], cb)` — array of deps - `module.hot.decline(dep)` / `import.meta.turbopackHot.decline(dep)` — single dep - `module.hot.decline([depA, depB])` / `import.meta.turbopackHot.decline([depA, depB])` — array of deps ### Why? Libraries like `react-refresh` and user code often need to accept updates for specific dependencies rather than self-accepting the entire module. Without this, those HMR patterns fall back to full page reloads in Turbopack. ### How? **Runtime** (`turbopack-ecmascript-runtime`): - Extended `HotState` with `acceptedDependencies`, `acceptedErrorHandlers`, and `declinedDependencies` maps (keyed by `ModuleId`) - Updated `hot.accept()` and `hot.decline()` to handle string and array signatures - Updated `getAffectedModuleEffects` to check accepted/declined dependencies when propagating updates through the module graph - Added `'declined'` effect type that throws an `UpdateApplyError` - Track `outdatedDependencies` (map of parent → set of updated deps) alongside `outdatedModules` - In the apply phase, invoke per-dependency accept callbacks with the correct outdated deps - Use `Set` for `outdatedDependencies` dedup (O(1) vs O(n) lookups) **Compiler** (`turbopack-ecmascript`): - Added `ModuleHotReferenceAssetReference` — a single asset reference type for both accept and decline deps, with shared resolve logic for ESM (`esm_resolve`) and CJS (`cjs_resolve`) - Added `ModuleHotReferenceCodeGen` — generates code that replaces dep string literals with resolved module IDs at compile time - ESM binding auto-update: when an ESM module accepts a dependency that it also `import`s, the compiler wraps the accept callback to re-import the namespace variable (`__TURBOPACK__imported__module__<id> = __turbopack_import__(<id>)`) before the user callback runs, so ESM bindings reflect updated values without needing `require()` - Added `import.meta.turbopackHot` as the ESM equivalent of `module.hot`, with TypeScript type declarations in `packages/next/types/global.d.ts` - Static analysis extracts dep strings from `module.hot.accept`/`decline` calls; non-analyzable deps emit a warning with distinct error codes (`TP1204` for accept, `TP1205` for decline) **HMR gating** (`CompileTimeInfo`): - Added `hot_module_replacement_enabled` flag to `CompileTimeInfo` to gate recognition of `module.hot` and `import.meta.turbopackHot` as well-known objects - Without this flag, production builds would recognize `module.hot.accept(...)` and generate HMR-specific code, leading to runtime errors - Flag set to `true` for dev servers and `false` for production builds across Next.js and turbopack-cli entry points - `import.meta.turbopackHot` getter is only emitted when HMR is enabled **Server HMR** (`next-api`, `next-core`): - Server-side `compile_time_info` now also sets `hot_module_replacement_enabled` so that `module.hot` / `import.meta.turbopackHot` are recognized during analysis of server modules - Server HMR requires the `--experimental-server-fast-refresh` CLI flag; the flag is passed through `ProjectOptions` to the Rust side so `server_compile_time_info` only enables HMR when appropriate **Tests** (`test/development/app-dir/hmr-dep-accept/`): - ESM single dep accept — verifies parent module is not re-evaluated, accept callback fires, ESM bindings auto-update - ESM array dep accept — same as above with `accept(['./dep-a', './dep-b'], cb)` - CJS `module.hot.accept` — pure CJS dep observer pattern with `.cjs` files - Single dep decline — verifies full page reload occurs - Array dep decline — verifies full page reload with `decline(['./dep-a', './dep-b'])`
Author
Parents
Loading