Turbopack: extract core hmr client logic into shared runtime (#88912)

This PR mostly moves existing code. Most of the diff churn is from re-recording shapshots. This PR is nowhere as scary as the diffstat would have you believe.

This extracts the core HMR client logic, abstracting away hooks for platform-specific features like the websocket transport and page reload.
    
In a future PR this will be implemented as an in-process backend for nodejs server hmr.
    
Test Plan: CI, existing hmr tests
This commit is contained in:
Will Binns-Smith
2026-02-10 13:33:41 -08:00
committed by GitHub
parent f7dd09fd5f
commit d77eb04091
26 changed files with 4885 additions and 3720 deletions
@@ -1,7 +1,7 @@
/// <reference path="../../../shared/runtime-types.d.ts" />
/// <reference path="../../runtime/base/dev-globals.d.ts" />
/// <reference path="../../runtime/base/dev-protocol.d.ts" />
/// <reference path="../../runtime/base/dev-extensions.ts" />
/// <reference path="../../../shared/runtime/runtime-types.d.ts" />
/// <reference path="../../../shared/runtime/dev-globals.d.ts" />
/// <reference path="../../../shared/runtime/dev-protocol.d.ts" />
/// <reference path="../../../shared/runtime/dev-extensions.ts" />
type SendMessage = (msg: any) => void
export type WebSocketMessage =
@@ -1 +0,0 @@
export * from './hmr-client'
@@ -14,11 +14,12 @@ const devContextPrototype = Context.prototype as TurbopackDevContext
* It will be appended to the runtime code of each runtime right after the
* shared runtime utils.
*/
/* eslint-disable @typescript-eslint/no-unused-vars */
const devModuleCache: ModuleCache<HotModule> = Object.create(null)
// Assign browser's module cache and runtime modules to shared HMR state
devModuleCache = Object.create(null)
devContextPrototype.c = devModuleCache
runtimeModules = new Set()
// This file must not use `import` and `export` statements. Otherwise, it
// becomes impossible to augment interfaces declared in `<reference>`d files
@@ -50,22 +51,6 @@ interface DevRuntimeBackend {
restart: () => void
}
class UpdateApplyError extends Error {
name = 'UpdateApplyError'
dependencyChain: ModuleId[]
constructor(message: string, dependencyChain: ModuleId[]) {
super(message)
this.dependencyChain = dependencyChain
}
}
/**
* Module IDs that are instantiated as part of the runtime of a chunk.
*/
const runtimeModules: Set<ModuleId> = new Set()
/**
* Map from module ID to the chunks that contain this module.
*
@@ -93,20 +78,6 @@ const chunkListChunksMap: Map<ChunkListPath, Set<ChunkPath>> = new Map()
*/
const chunkChunkListsMap: Map<ChunkPath, Set<ChunkListPath>> = new Map()
/**
* Maps module IDs to persisted data between executions of their hot module
* implementation (`hot.data`).
*/
const moduleHotData: Map<ModuleId, HotData> = new Map()
/**
* Maps module instances to their hot module state.
*/
const moduleHotState: Map<Module, HotState> = new Map()
/**
* Modules that call `module.hot.invalidate()` (while being updated).
*/
const queuedInvalidatedModules: Set<ModuleId> = new Set()
/**
* Gets or instantiates a runtime module.
*/
@@ -185,74 +156,36 @@ function instantiateModule(
sourceType: SourceType,
sourceData: SourceData
): Module {
// We are in development, this is always a string.
let id = moduleId as string
// Browser: creates base HotModule object (hot API added by shared code)
const createModuleObjectFn = (id: ModuleId) => {
return createModuleObject(id) as HotModule
}
const moduleFactory = moduleFactories.get(id)
if (typeof moduleFactory !== 'function') {
// This can happen if modules incorrectly handle HMR disposes/updates,
// e.g. when they keep a `setTimeout` around which still executes old code
// and contains e.g. a `require("something")` call.
throw new Error(
factoryNotAvailableMessage(id, sourceType, sourceData) +
' It might have been deleted in an HMR update.'
// Browser: creates DevContext with refresh
const createContext = (
module: HotModule,
exports: Exports,
refresh: RefreshContext
) => {
return new (DevContext as any as DevContextConstructor)(
module,
exports,
refresh
)
}
const hotData = moduleHotData.get(id)!
const { hot, hotState } = createModuleHot(id, hotData)
let parents: ModuleId[]
switch (sourceType) {
case SourceType.Runtime:
runtimeModules.add(id)
parents = []
break
case SourceType.Parent:
// No need to add this module as a child of the parent module here, this
// has already been taken care of in `getOrInstantiateModuleFromParent`.
parents = [sourceData as ModuleId]
break
case SourceType.Update:
parents = (sourceData as ModuleId[]) || []
break
default:
invariant(
sourceType,
(sourceType) => `Unknown source type: ${sourceType}`
)
}
const module: HotModule = createModuleObject(id) as HotModule
const exports = module.exports
module.parents = parents
module.children = []
module.hot = hot
devModuleCache[id] = module
moduleHotState.set(module, hotState)
// NOTE(alexkirsz) This can fail when the module encounters a runtime error.
try {
runModuleExecutionHooks(module, (refresh) => {
const context = new (DevContext as any as DevContextConstructor)(
module,
exports,
refresh
)
moduleFactory(context, module, exports)
})
} catch (error) {
module.error = error as any
throw error
}
if (module.namespaceObject && module.exports !== module.namespaceObject) {
// in case of a circular dependency: cjs1 -> esm2 -> cjs1
interopEsm(module.exports, module.namespaceObject)
}
return module
// Use shared instantiation logic (includes hot API setup)
return instantiateModuleShared(
moduleId,
sourceType,
sourceData,
moduleFactories,
devModuleCache,
runtimeModules,
createModuleObjectFn,
createContext,
runModuleExecutionHooks
)
}
const DUMMY_REFRESH_CONTEXT = {
@@ -349,91 +282,6 @@ function registerExportsAndSetupBoundaryForReactRefresh(
}
}
function formatDependencyChain(dependencyChain: ModuleId[]): string {
return `Dependency chain: ${dependencyChain.join(' -> ')}`
}
function computeOutdatedModules(
added: Map<ModuleId, EcmascriptModuleEntry | undefined>,
modified: Map<ModuleId, EcmascriptModuleEntry>
): {
outdatedModules: Set<ModuleId>
newModuleFactories: Map<ModuleId, ModuleFactory>
} {
const newModuleFactories = new Map<ModuleId, ModuleFactory>()
for (const [moduleId, entry] of added) {
if (entry != null) {
newModuleFactories.set(moduleId, _eval(entry))
}
}
const outdatedModules = computedInvalidatedModules(modified.keys())
for (const [moduleId, entry] of modified) {
newModuleFactories.set(moduleId, _eval(entry))
}
return { outdatedModules, newModuleFactories }
}
function computedInvalidatedModules(
invalidated: Iterable<ModuleId>
): Set<ModuleId> {
const outdatedModules = new Set<ModuleId>()
for (const moduleId of invalidated) {
const effect = getAffectedModuleEffects(moduleId)
switch (effect.type) {
case 'unaccepted':
throw new UpdateApplyError(
`cannot apply update: unaccepted module. ${formatDependencyChain(
effect.dependencyChain
)}.`,
effect.dependencyChain
)
case 'self-declined':
throw new UpdateApplyError(
`cannot apply update: self-declined module. ${formatDependencyChain(
effect.dependencyChain
)}.`,
effect.dependencyChain
)
case 'accepted':
for (const outdatedModuleId of effect.outdatedModules) {
outdatedModules.add(outdatedModuleId)
}
break
// TODO(alexkirsz) Dependencies: handle dependencies effects.
default:
invariant(effect, (effect) => `Unknown effect type: ${effect?.type}`)
}
}
return outdatedModules
}
function computeOutdatedSelfAcceptedModules(
outdatedModules: Iterable<ModuleId>
): { moduleId: ModuleId; errorHandler: true | Function }[] {
const outdatedSelfAcceptedModules: {
moduleId: ModuleId
errorHandler: true | Function
}[] = []
for (const moduleId of outdatedModules) {
const module = devModuleCache[moduleId]
const hotState = moduleHotState.get(module)!
if (module && hotState.selfAccepted && !hotState.selfInvalidated) {
outdatedSelfAcceptedModules.push({
moduleId,
errorHandler: hotState.selfAccepted,
})
}
}
return outdatedSelfAcceptedModules
}
/**
* Adds, deletes, and moves modules between chunks. This must happen before the
* dispose phase as it needs to know which modules were removed from all chunks,
@@ -461,139 +309,6 @@ function updateChunksPhase(
return { disposedModules }
}
function disposePhase(
outdatedModules: Iterable<ModuleId>,
disposedModules: Iterable<ModuleId>
): { outdatedModuleParents: Map<ModuleId, Array<ModuleId>> } {
for (const moduleId of outdatedModules) {
disposeModule(moduleId, 'replace')
}
for (const moduleId of disposedModules) {
disposeModule(moduleId, 'clear')
}
// Removing modules from the module cache is a separate step.
// We also want to keep track of previous parents of the outdated modules.
const outdatedModuleParents = new Map()
for (const moduleId of outdatedModules) {
const oldModule = devModuleCache[moduleId]
outdatedModuleParents.set(moduleId, oldModule?.parents)
delete devModuleCache[moduleId]
}
// TODO(alexkirsz) Dependencies: remove outdated dependency from module
// children.
return { outdatedModuleParents }
}
/**
* Disposes of an instance of a module.
*
* Returns the persistent hot data that should be kept for the next module
* instance.
*
* NOTE: mode = "replace" will not remove modules from the devModuleCache
* This must be done in a separate step afterwards.
* This is important because all modules need to be disposed to update the
* parent/child relationships before they are actually removed from the devModuleCache.
* If this was done in this method, the following disposeModule calls won't find
* the module from the module id in the cache.
*/
function disposeModule(moduleId: ModuleId, mode: 'clear' | 'replace') {
const module = devModuleCache[moduleId]
if (!module) {
return
}
const hotState = moduleHotState.get(module)!
const data = {}
// Run the `hot.dispose` handler, if any, passing in the persistent
// `hot.data` object.
for (const disposeHandler of hotState.disposeHandlers) {
disposeHandler(data)
}
// This used to warn in `getOrInstantiateModuleFromParent` when a disposed
// module is still importing other modules.
module.hot.active = false
moduleHotState.delete(module)
// TODO(alexkirsz) Dependencies: delete the module from outdated deps.
// Remove the disposed module from its children's parent list.
// It will be added back once the module re-instantiates and imports its
// children again.
for (const childId of module.children) {
const child = devModuleCache[childId]
if (!child) {
continue
}
const idx = child.parents.indexOf(module.id)
if (idx >= 0) {
child.parents.splice(idx, 1)
}
}
switch (mode) {
case 'clear':
delete devModuleCache[module.id]
moduleHotData.delete(module.id)
break
case 'replace':
moduleHotData.set(module.id, data)
break
default:
invariant(mode, (mode) => `invalid mode: ${mode}`)
}
}
function applyPhase(
outdatedSelfAcceptedModules: {
moduleId: ModuleId
errorHandler: true | Function
}[],
newModuleFactories: Map<ModuleId, ModuleFactory>,
outdatedModuleParents: Map<ModuleId, Array<ModuleId>>,
reportError: (err: any) => void
) {
// Update module factories.
for (const [moduleId, factory] of newModuleFactories.entries()) {
applyModuleFactoryName(factory)
moduleFactories.set(moduleId, factory)
}
// TODO(alexkirsz) Run new runtime entries here.
// TODO(alexkirsz) Dependencies: call accept handlers for outdated deps.
// Re-instantiate all outdated self-accepted modules.
for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) {
try {
instantiateModule(
moduleId,
SourceType.Update,
outdatedModuleParents.get(moduleId)
)
} catch (err) {
if (typeof errorHandler === 'function') {
try {
errorHandler(err, { moduleId, module: devModuleCache[moduleId] })
} catch (err2) {
reportError(err2)
reportError(err)
}
} else {
reportError(err)
}
}
}
}
function applyUpdate(update: PartialUpdate) {
switch (update.type) {
case 'ChunkListUpdate':
@@ -651,254 +366,26 @@ function applyChunkListUpdate(update: ChunkListUpdate) {
}
function applyEcmascriptMergedUpdate(update: EcmascriptMergedUpdate) {
// Browser-specific chunk management phase
const { entries = {}, chunks = {} } = update
const { added, modified, chunksAdded, chunksDeleted } = computeChangedModules(
entries,
chunks
)
const { outdatedModules, newModuleFactories } = computeOutdatedModules(
added,
modified
chunks,
chunkModulesMap
)
const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted)
applyInternal(outdatedModules, disposedModules, newModuleFactories)
}
function applyInvalidatedModules(outdatedModules: Set<ModuleId>) {
if (queuedInvalidatedModules.size > 0) {
computedInvalidatedModules(queuedInvalidatedModules).forEach((moduleId) => {
outdatedModules.add(moduleId)
})
queuedInvalidatedModules.clear()
}
return outdatedModules
}
function applyInternal(
outdatedModules: Set<ModuleId>,
disposedModules: Iterable<ModuleId>,
newModuleFactories: Map<ModuleId, ModuleFactory>
) {
outdatedModules = applyInvalidatedModules(outdatedModules)
const outdatedSelfAcceptedModules =
computeOutdatedSelfAcceptedModules(outdatedModules)
const { outdatedModuleParents } = disposePhase(
outdatedModules,
disposedModules
)
// we want to continue on error and only throw the error after we tried applying all updates
let error: any
function reportError(err: any) {
if (!error) error = err
}
applyPhase(
outdatedSelfAcceptedModules,
newModuleFactories,
outdatedModuleParents,
reportError
)
if (error) {
throw error
}
if (queuedInvalidatedModules.size > 0) {
applyInternal(new Set(), [], new Map())
}
}
function computeChangedModules(
entries: Record<ModuleId, EcmascriptModuleEntry>,
updates: Record<ChunkPath, EcmascriptMergedChunkUpdate>
): {
added: Map<ModuleId, EcmascriptModuleEntry | undefined>
modified: Map<ModuleId, EcmascriptModuleEntry>
deleted: Set<ModuleId>
chunksAdded: Map<ChunkPath, Set<ModuleId>>
chunksDeleted: Map<ChunkPath, Set<ModuleId>>
} {
const chunksAdded = new Map()
const chunksDeleted = new Map()
const added: Map<ModuleId, EcmascriptModuleEntry> = new Map()
const modified = new Map()
const deleted: Set<ModuleId> = new Set()
for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates) as Array<
[ChunkPath, EcmascriptMergedChunkUpdate]
>) {
switch (mergedChunkUpdate.type) {
case 'added': {
const updateAdded = new Set(mergedChunkUpdate.modules)
for (const moduleId of updateAdded) {
added.set(moduleId, entries[moduleId])
}
chunksAdded.set(chunkPath, updateAdded)
break
}
case 'deleted': {
// We could also use `mergedChunkUpdate.modules` here.
const updateDeleted = new Set(chunkModulesMap.get(chunkPath))
for (const moduleId of updateDeleted) {
deleted.add(moduleId)
}
chunksDeleted.set(chunkPath, updateDeleted)
break
}
case 'partial': {
const updateAdded = new Set(mergedChunkUpdate.added)
const updateDeleted = new Set(mergedChunkUpdate.deleted)
for (const moduleId of updateAdded) {
added.set(moduleId, entries[moduleId])
}
for (const moduleId of updateDeleted) {
deleted.add(moduleId)
}
chunksAdded.set(chunkPath, updateAdded)
chunksDeleted.set(chunkPath, updateDeleted)
break
}
default:
invariant(
mergedChunkUpdate,
(mergedChunkUpdate) =>
`Unknown merged chunk update type: ${mergedChunkUpdate.type}`
)
}
}
// If a module was added from one chunk and deleted from another in the same update,
// consider it to be modified, as it means the module was moved from one chunk to another
// AND has new code in a single update.
for (const moduleId of added.keys()) {
if (deleted.has(moduleId)) {
added.delete(moduleId)
deleted.delete(moduleId)
}
}
for (const [moduleId, entry] of Object.entries(entries)) {
// Modules that haven't been added to any chunk but have new code are considered
// to be modified.
// This needs to be under the previous loop, as we need it to get rid of modules
// that were added and deleted in the same update.
if (!added.has(moduleId)) {
modified.set(moduleId, entry)
}
}
return { added, deleted, modified, chunksAdded, chunksDeleted }
}
type ModuleEffect =
| {
type: 'unaccepted'
dependencyChain: ModuleId[]
}
| {
type: 'self-declined'
dependencyChain: ModuleId[]
moduleId: ModuleId
}
| {
type: 'accepted'
moduleId: ModuleId
outdatedModules: Set<ModuleId>
}
function getAffectedModuleEffects(moduleId: ModuleId): ModuleEffect {
const outdatedModules: Set<ModuleId> = new Set()
type QueueItem = { moduleId?: ModuleId; dependencyChain: ModuleId[] }
const queue: QueueItem[] = [
{
moduleId,
dependencyChain: [],
},
]
let nextItem
while ((nextItem = queue.shift())) {
const { moduleId, dependencyChain } = nextItem
if (moduleId != null) {
if (outdatedModules.has(moduleId)) {
// Avoid infinite loops caused by cycles between modules in the dependency chain.
continue
}
outdatedModules.add(moduleId)
}
// We've arrived at the runtime of the chunk, which means that nothing
// else above can accept this update.
if (moduleId === undefined) {
return {
type: 'unaccepted',
dependencyChain,
}
}
const module = devModuleCache[moduleId]
const hotState = moduleHotState.get(module)!
if (
// The module is not in the cache. Since this is a "modified" update,
// it means that the module was never instantiated before.
!module || // The module accepted itself without invalidating globalThis.
// TODO is that right?
(hotState.selfAccepted && !hotState.selfInvalidated)
) {
continue
}
if (hotState.selfDeclined) {
return {
type: 'self-declined',
dependencyChain,
moduleId,
}
}
if (runtimeModules.has(moduleId)) {
queue.push({
moduleId: undefined,
dependencyChain: [...dependencyChain, moduleId],
})
continue
}
for (const parentId of module.parents) {
const parent = devModuleCache[parentId]
if (!parent) {
// TODO(alexkirsz) Is this even possible?
continue
}
// TODO(alexkirsz) Dependencies: check accepted and declined
// dependencies here.
queue.push({
moduleId: parentId,
dependencyChain: [...dependencyChain, moduleId],
})
}
}
return {
type: 'accepted',
moduleId,
outdatedModules,
}
// Use shared HMR update implementation
applyEcmascriptMergedUpdateShared({
added,
modified,
disposedModules,
evalModuleEntry: _eval, // browser's eval with source maps
instantiateModule,
applyModuleFactoryName,
moduleFactories,
devModuleCache,
})
}
function handleApply(chunkListPath: ChunkListPath, update: ServerMessage) {
@@ -932,86 +419,6 @@ function handleApply(chunkListPath: ChunkListPath, update: ServerMessage) {
}
}
function createModuleHot(
moduleId: ModuleId,
hotData: HotData
): { hot: Hot; hotState: HotState } {
const hotState: HotState = {
selfAccepted: false,
selfDeclined: false,
selfInvalidated: false,
disposeHandlers: [],
}
const hot: Hot = {
// TODO(alexkirsz) This is not defined in the HMR API. It was used to
// decide whether to warn whenever an HMR-disposed module required other
// modules. We might want to remove it.
active: true,
data: hotData ?? {},
// TODO(alexkirsz) Support full (dep, callback, errorHandler) form.
accept: (
modules?: string | string[] | AcceptErrorHandler,
_callback?: AcceptCallback,
_errorHandler?: AcceptErrorHandler
) => {
if (modules === undefined) {
hotState.selfAccepted = true
} else if (typeof modules === 'function') {
hotState.selfAccepted = modules
} else {
throw new Error('unsupported `accept` signature')
}
},
decline: (dep) => {
if (dep === undefined) {
hotState.selfDeclined = true
} else {
throw new Error('unsupported `decline` signature')
}
},
dispose: (callback) => {
hotState.disposeHandlers.push(callback)
},
addDisposeHandler: (callback) => {
hotState.disposeHandlers.push(callback)
},
removeDisposeHandler: (callback) => {
const idx = hotState.disposeHandlers.indexOf(callback)
if (idx >= 0) {
hotState.disposeHandlers.splice(idx, 1)
}
},
invalidate: () => {
hotState.selfInvalidated = true
queuedInvalidatedModules.add(moduleId)
},
// NOTE(alexkirsz) This is part of the management API, which we don't
// implement, but the Next.js React Refresh runtime uses this to decide
// whether to schedule an update.
status: () => 'idle',
// NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops.
addStatusHandler: (_handler) => {},
removeStatusHandler: (_handler) => {},
// NOTE(jridgewell) Check returns the list of updated modules, but we don't
// want the webpack code paths to ever update (the turbopack paths handle
// this already).
check: () => Promise.resolve(null),
}
return { hot, hotState }
}
/**
* Removes a module from a chunk.
* Returns `true` if there are no remaining chunks including this module.
@@ -58,27 +58,6 @@ type ChunkList = {
source: 'entry' | 'dynamic'
}
enum SourceType {
/**
* The module was instantiated because it was included in an evaluated chunk's
* runtime.
* SourceData is a ChunkPath.
*/
Runtime = 0,
/**
* The module was instantiated because a parent module imported it.
* SourceData is a ModuleId.
*/
Parent = 1,
/**
* The module was instantiated because it was included in a chunk's hot module
* update.
* SourceData is an array of ModuleIds or undefined.
*/
Update = 2,
}
type SourceData = ChunkPath | ModuleId | ModuleId[] | undefined
interface RuntimeBackend {
registerChunk: (
chunkPath: ChunkPath | ChunkScript,
@@ -0,0 +1,827 @@
/// <reference path="./runtime-utils.ts" />
/// <reference path="./runtime-types.d.ts" />
/// <reference path="./dev-extensions.ts" />
/// <reference path="./dev-protocol.d.ts" />
type HotModuleFactoryFunction = ModuleFactoryFunction<
HotModule,
TurbopackBaseContext<HotModule>
>
/**
* Shared HMR (Hot Module Replacement) implementation.
*
* This file contains the complete HMR implementation that's shared between
* browser and Node.js runtimes. It manages module hot state, dependency
* tracking, the module.hot API, and the full HMR update flow.
*/
/**
* The development module cache shared across the runtime.
* Browser runtime declares this directly.
* Node.js runtime assigns globalThis.__turbopack_module_cache__ to this.
*/
let devModuleCache: Record<ModuleId, any>
/**
* Module IDs that are instantiated as part of the runtime of a chunk.
*/
let runtimeModules: Set<ModuleId>
/**
* Maps module IDs to persisted data between executions of their hot module
* implementation (`hot.data`).
*/
const moduleHotData: Map<ModuleId, HotData> = new Map()
/**
* Maps module instances to their hot module state.
* Uses WeakMap so it works with both HotModule and ModuleWithDirection.
*/
const moduleHotState: WeakMap<any, HotState> = new WeakMap()
/**
* Modules that call `module.hot.invalidate()` (while being updated).
*/
const queuedInvalidatedModules: Set<ModuleId> = new Set()
class UpdateApplyError extends Error {
name = 'UpdateApplyError'
dependencyChain: ModuleId[]
constructor(message: string, dependencyChain: ModuleId[]) {
super(message)
this.dependencyChain = dependencyChain
}
}
type ModuleEffect =
| {
type: 'unaccepted'
dependencyChain: ModuleId[]
}
| {
type: 'self-declined'
dependencyChain: ModuleId[]
moduleId: ModuleId
}
| {
type: 'accepted'
moduleId: ModuleId
outdatedModules: Set<ModuleId>
}
/**
* Records parent-child relationship when a module imports another.
* Should be called during module instantiation.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function trackModuleImport(
parentModule: ModuleWithDirection,
childModuleId: ModuleId,
childModule: ModuleWithDirection | undefined
): void {
// Record that parent imports child
if (parentModule.children.indexOf(childModuleId) === -1) {
parentModule.children.push(childModuleId)
}
// Record that child is imported by parent
if (childModule && childModule.parents.indexOf(parentModule.id) === -1) {
childModule.parents.push(parentModule.id)
}
}
function formatDependencyChain(dependencyChain: ModuleId[]): string {
return `Dependency chain: ${dependencyChain.join(' -> ')}`
}
/**
* Walks the dependency tree to find all modules affected by a change.
* Returns information about whether the update can be accepted and which
* modules need to be invalidated.
*
* Copied directly from browser implementation.
*
* @param moduleId - The module to check for effects
*/
function getAffectedModuleEffects(moduleId: ModuleId): ModuleEffect {
const outdatedModules: Set<ModuleId> = new Set()
type QueueItem = { moduleId?: ModuleId; dependencyChain: ModuleId[] }
const queue: QueueItem[] = [
{
moduleId,
dependencyChain: [],
},
]
let nextItem
while ((nextItem = queue.shift())) {
const { moduleId: currentModuleId, dependencyChain } = nextItem
// We've arrived at the runtime of the chunk, which means that nothing
// else above can accept this update.
if (currentModuleId === undefined) {
return {
type: 'unaccepted',
dependencyChain,
}
}
if (outdatedModules.has(currentModuleId)) {
// Avoid infinite loops caused by cycles between modules in the dependency chain.
continue
}
outdatedModules.add(currentModuleId)
const module = devModuleCache[currentModuleId]
const hotState = moduleHotState.get(module)!
if (
// The module is not in the cache. Since this is a "modified" update,
// it means that the module was never instantiated before.
!module || // The module accepted itself without invalidating globalThis.
// TODO is that right?
(hotState.selfAccepted && !hotState.selfInvalidated)
) {
continue
}
if (hotState.selfDeclined) {
return {
type: 'self-declined',
dependencyChain,
moduleId: currentModuleId,
}
}
if (runtimeModules.has(currentModuleId)) {
queue.push({
moduleId: undefined,
dependencyChain: [...dependencyChain, currentModuleId],
})
continue
}
for (const parentId of module.parents) {
const parent = devModuleCache[parentId]
if (!parent) {
// TODO(alexkirsz) Is this even possible?
continue
}
// TODO(alexkirsz) Dependencies: check accepted and declined
// dependencies here.
queue.push({
moduleId: parentId,
dependencyChain: [...dependencyChain, currentModuleId],
})
}
}
return {
type: 'accepted',
moduleId,
outdatedModules,
}
}
/**
* Computes all modules that need to be invalidated based on which modules changed.
*
* Copied directly from browser implementation.
*
* @param invalidated - The modules to check for invalidation
*/
function computedInvalidatedModules(
invalidated: Iterable<ModuleId>
): Set<ModuleId> {
const outdatedModules = new Set<ModuleId>()
for (const moduleId of invalidated) {
const effect = getAffectedModuleEffects(moduleId)
switch (effect.type) {
case 'unaccepted':
throw new UpdateApplyError(
`cannot apply update: unaccepted module. ${formatDependencyChain(
effect.dependencyChain
)}.`,
effect.dependencyChain
)
case 'self-declined':
throw new UpdateApplyError(
`cannot apply update: self-declined module. ${formatDependencyChain(
effect.dependencyChain
)}.`,
effect.dependencyChain
)
case 'accepted':
for (const outdatedModuleId of effect.outdatedModules) {
outdatedModules.add(outdatedModuleId)
}
break
// TODO(alexkirsz) Dependencies: handle dependencies effects.
default:
invariant(effect, (effect) => `Unknown effect type: ${effect?.type}`)
}
}
return outdatedModules
}
/**
* Creates the module.hot API object and its internal state.
* This provides the HMR API that user code calls (module.hot.accept(), etc.)
*/
function createModuleHot(
moduleId: ModuleId,
hotData: HotData
): { hot: Hot; hotState: HotState } {
const hotState: HotState = {
selfAccepted: false,
selfDeclined: false,
selfInvalidated: false,
disposeHandlers: [],
}
const hot: Hot = {
// TODO(alexkirsz) This is not defined in the HMR API. It was used to
// decide whether to warn whenever an HMR-disposed module required other
// modules. We might want to remove it.
active: true,
data: hotData ?? {},
// TODO(alexkirsz) Support full (dep, callback, errorHandler) form.
accept: (
modules?: string | string[] | AcceptErrorHandler,
_callback?: AcceptCallback,
_errorHandler?: AcceptErrorHandler
) => {
if (modules === undefined) {
hotState.selfAccepted = true
} else if (typeof modules === 'function') {
hotState.selfAccepted = modules
} else {
throw new Error('unsupported `accept` signature')
}
},
decline: (dep) => {
if (dep === undefined) {
hotState.selfDeclined = true
} else {
throw new Error('unsupported `decline` signature')
}
},
dispose: (callback) => {
hotState.disposeHandlers.push(callback)
},
addDisposeHandler: (callback) => {
hotState.disposeHandlers.push(callback)
},
removeDisposeHandler: (callback) => {
const idx = hotState.disposeHandlers.indexOf(callback)
if (idx >= 0) {
hotState.disposeHandlers.splice(idx, 1)
}
},
invalidate: () => {
hotState.selfInvalidated = true
queuedInvalidatedModules.add(moduleId)
},
// NOTE(alexkirsz) This is part of the management API, which we don't
// implement, but the Next.js React Refresh runtime uses this to decide
// whether to schedule an update.
status: () => 'idle',
// NOTE(alexkirsz) Since we always return "idle" for now, these are no-ops.
addStatusHandler: (_handler) => {},
removeStatusHandler: (_handler) => {},
// NOTE(jridgewell) Check returns the list of updated modules, but we don't
// want the webpack code paths to ever update (the turbopack paths handle
// this already).
check: () => Promise.resolve(null),
}
return { hot, hotState }
}
/**
* Processes queued invalidated modules and adds them to the outdated modules set.
* Modules that call module.hot.invalidate() are queued and processed here.
*
* @param outdatedModules - The set of already outdated modules
*/
function applyInvalidatedModules(
outdatedModules: Set<ModuleId>
): Set<ModuleId> {
if (queuedInvalidatedModules.size > 0) {
computedInvalidatedModules(queuedInvalidatedModules).forEach((moduleId) => {
outdatedModules.add(moduleId)
})
queuedInvalidatedModules.clear()
}
return outdatedModules
}
/**
* Computes which outdated modules have self-accepted and can be hot reloaded.
*/
function computeOutdatedSelfAcceptedModules(
outdatedModules: Iterable<ModuleId>
): { moduleId: ModuleId; errorHandler: true | Function }[] {
const outdatedSelfAcceptedModules: {
moduleId: ModuleId
errorHandler: true | Function
}[] = []
for (const moduleId of outdatedModules) {
const module = devModuleCache[moduleId]
const hotState = moduleHotState.get(module)
if (module && hotState?.selfAccepted && !hotState.selfInvalidated) {
outdatedSelfAcceptedModules.push({
moduleId,
errorHandler: hotState.selfAccepted,
})
}
}
return outdatedSelfAcceptedModules
}
/**
* Disposes of an instance of a module.
* Runs hot.dispose handlers and manages persistent hot data.
*
* NOTE: mode = "replace" will not remove modules from devModuleCache.
* This must be done in a separate step afterwards.
*/
function disposeModule(moduleId: ModuleId, mode: 'clear' | 'replace') {
const module = devModuleCache[moduleId]
if (!module) {
return
}
const hotState = moduleHotState.get(module)
if (!hotState) {
return
}
const data: HotData = {}
// Run the `hot.dispose` handler, if any, passing in the persistent
// `hot.data` object.
for (const disposeHandler of hotState.disposeHandlers) {
disposeHandler(data)
}
// This used to warn in `getOrInstantiateModuleFromParent` when a disposed
// module is still importing other modules.
if (module.hot) {
module.hot.active = false
}
moduleHotState.delete(module)
// TODO(alexkirsz) Dependencies: delete the module from outdated deps.
// Remove the disposed module from its children's parent list.
// It will be added back once the module re-instantiates and imports its
// children again.
for (const childId of module.children) {
const child = devModuleCache[childId]
if (!child) {
continue
}
const idx = child.parents.indexOf(module.id)
if (idx >= 0) {
child.parents.splice(idx, 1)
}
}
switch (mode) {
case 'clear':
delete devModuleCache[module.id]
moduleHotData.delete(module.id)
break
case 'replace':
moduleHotData.set(module.id, data)
break
default:
invariant(mode, (mode) => `invalid mode: ${mode}`)
}
}
/**
* Dispose phase: runs dispose handlers and cleans up outdated/disposed modules.
* Returns the parent modules of outdated modules for use in the apply phase.
*/
function disposePhase(
outdatedModules: Iterable<ModuleId>,
disposedModules: Iterable<ModuleId>
): { outdatedModuleParents: Map<ModuleId, Array<ModuleId>> } {
for (const moduleId of outdatedModules) {
disposeModule(moduleId, 'replace')
}
for (const moduleId of disposedModules) {
disposeModule(moduleId, 'clear')
}
// Removing modules from the module cache is a separate step.
// We also want to keep track of previous parents of the outdated modules.
const outdatedModuleParents = new Map<ModuleId, Array<ModuleId>>()
for (const moduleId of outdatedModules) {
const oldModule = devModuleCache[moduleId]
outdatedModuleParents.set(moduleId, oldModule?.parents)
delete devModuleCache[moduleId]
}
// TODO(alexkirsz) Dependencies: remove outdated dependency from module
// children.
return { outdatedModuleParents }
}
/* eslint-disable @typescript-eslint/no-unused-vars */
/**
* Shared module instantiation logic.
* This handles the full module instantiation flow for both browser and Node.js.
* Only React Refresh hooks differ between platforms (passed as callback).
*/
function instantiateModuleShared(
moduleId: ModuleId,
sourceType: SourceType,
sourceData: SourceData,
moduleFactories: ModuleFactories,
devModuleCache: ModuleCache<HotModule>,
runtimeModules: Set<ModuleId>,
createModuleObjectFn: (id: ModuleId) => HotModule,
createContextFn: (module: HotModule, exports: Exports, refresh?: any) => any,
runModuleExecutionHooksFn: (
module: HotModule,
exec: (refresh: any) => void
) => void
): HotModule {
// 1. Factory validation (same in both browser and Node.js)
const id = moduleId as string
const moduleFactory = moduleFactories.get(id)
if (typeof moduleFactory !== 'function') {
throw new Error(
`Module ${id} was instantiated, but the module factory is not available.`
)
}
// 2. Hot API setup (same in both - works for browser, included for Node.js)
const hotData = moduleHotData.get(id)!
const { hot, hotState } = createModuleHot(id, hotData)
// 3. Parent assignment logic (same in both)
let parents: ModuleId[]
switch (sourceType) {
case SourceType.Runtime:
runtimeModules.add(id)
parents = []
break
case SourceType.Parent:
parents = [sourceData as ModuleId]
break
case SourceType.Update:
parents = (sourceData as ModuleId[]) || []
break
default:
throw new Error(`Unknown source type: ${sourceType}`)
}
// 4. Module creation (platform creates base module object)
const module = createModuleObjectFn(id)
const exports = module.exports
module.parents = parents
module.children = []
module.hot = hot
devModuleCache[id] = module
moduleHotState.set(module, hotState)
// 5. Module execution (React Refresh hooks are platform-specific)
try {
runModuleExecutionHooksFn(module, (refresh) => {
const context = createContextFn(module, exports, refresh)
moduleFactory.call(exports, context, module, exports)
})
} catch (error) {
module.error = error as any
throw error
}
// 6. ESM interop (same in both)
if (module.namespaceObject && module.exports !== module.namespaceObject) {
// in case of a circular dependency: cjs1 -> esm2 -> cjs1
interopEsm(module.exports, module.namespaceObject)
}
return module
}
/**
* Analyzes update entries and chunks to determine which modules were added, modified, or deleted.
* This is pure logic that doesn't depend on the runtime environment.
*/
function computeChangedModules(
entries: Record<ModuleId, EcmascriptModuleEntry>,
updates: Record<ChunkPath, EcmascriptMergedChunkUpdate>,
chunkModulesMap?: Map<ChunkPath, Set<ModuleId>>
): {
added: Map<ModuleId, EcmascriptModuleEntry | undefined>
modified: Map<ModuleId, EcmascriptModuleEntry>
deleted: Set<ModuleId>
chunksAdded: Map<ChunkPath, Set<ModuleId>>
chunksDeleted: Map<ChunkPath, Set<ModuleId>>
} {
const chunksAdded = new Map()
const chunksDeleted = new Map()
const added: Map<ModuleId, EcmascriptModuleEntry> = new Map()
const modified = new Map()
const deleted: Set<ModuleId> = new Set()
for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates) as Array<
[ChunkPath, EcmascriptMergedChunkUpdate]
>) {
switch (mergedChunkUpdate.type) {
case 'added': {
const updateAdded = new Set(mergedChunkUpdate.modules)
for (const moduleId of updateAdded) {
added.set(moduleId, entries[moduleId])
}
chunksAdded.set(chunkPath, updateAdded)
break
}
case 'deleted': {
const updateDeleted = chunkModulesMap
? new Set(chunkModulesMap.get(chunkPath))
: new Set<ModuleId>()
for (const moduleId of updateDeleted) {
deleted.add(moduleId)
}
chunksDeleted.set(chunkPath, updateDeleted)
break
}
case 'partial': {
const updateAdded = new Set(mergedChunkUpdate.added)
const updateDeleted = new Set(mergedChunkUpdate.deleted)
for (const moduleId of updateAdded) {
added.set(moduleId, entries[moduleId])
}
for (const moduleId of updateDeleted) {
deleted.add(moduleId)
}
chunksAdded.set(chunkPath, updateAdded)
chunksDeleted.set(chunkPath, updateDeleted)
break
}
default:
throw new Error(`Unknown merged chunk update`)
}
}
// If a module was added from one chunk and deleted from another in the same update,
// consider it to be modified, as it means the module was moved from one chunk to another
// AND has new code in a single update.
for (const moduleId of added.keys()) {
if (deleted.has(moduleId)) {
added.delete(moduleId)
deleted.delete(moduleId)
}
}
for (const [moduleId, entry] of Object.entries(entries)) {
// Modules that haven't been added to any chunk but have new code are considered
// to be modified.
// This needs to be under the previous loop, as we need it to get rid of modules
// that were added and deleted in the same update.
if (!added.has(moduleId)) {
modified.set(moduleId, entry)
}
}
return { added, deleted, modified, chunksAdded, chunksDeleted }
}
/**
* Compiles new module code and walks the dependency tree to find all outdated modules.
* Uses the evalModuleEntry function to compile code (platform-specific).
*
* @param added - Map of added modules
* @param modified - Map of modified modules
* @param evalModuleEntry - Function to compile module code
*/
function computeOutdatedModules(
added: Map<ModuleId, EcmascriptModuleEntry | undefined>,
modified: Map<ModuleId, EcmascriptModuleEntry>,
evalModuleEntry: (entry: EcmascriptModuleEntry) => HotModuleFactoryFunction
): {
outdatedModules: Set<ModuleId>
newModuleFactories: Map<ModuleId, HotModuleFactoryFunction>
} {
const newModuleFactories = new Map<ModuleId, HotModuleFactoryFunction>()
// Compile added modules
for (const [moduleId, entry] of added) {
if (entry != null) {
newModuleFactories.set(moduleId, evalModuleEntry(entry))
}
}
// Walk dependency tree to find all modules affected by modifications
const outdatedModules = computedInvalidatedModules(modified.keys())
for (const [moduleId, entry] of modified) {
newModuleFactories.set(moduleId, evalModuleEntry(entry))
}
return { outdatedModules, newModuleFactories }
}
/**
* Updates module factories and re-instantiates self-accepted modules.
* Uses the instantiateModule function (platform-specific via callback).
*/
function applyPhase(
outdatedSelfAcceptedModules: {
moduleId: ModuleId
errorHandler: true | Function
}[],
newModuleFactories: Map<ModuleId, HotModuleFactoryFunction>,
outdatedModuleParents: Map<ModuleId, Array<ModuleId>>,
moduleFactories: ModuleFactories,
devModuleCache: ModuleCache<HotModule>,
instantiateModuleFn: (
moduleId: ModuleId,
sourceType: SourceType,
sourceData: SourceData
) => HotModule,
applyModuleFactoryNameFn: (factory: HotModuleFactoryFunction) => void,
reportError: (err: any) => void
) {
// Update module factories
for (const [moduleId, factory] of newModuleFactories.entries()) {
applyModuleFactoryNameFn(factory)
moduleFactories.set(moduleId, factory)
}
// TODO(alexkirsz) Run new runtime entries here.
// TODO(alexkirsz) Dependencies: call accept handlers for outdated deps.
// Re-instantiate all outdated self-accepted modules
for (const { moduleId, errorHandler } of outdatedSelfAcceptedModules) {
try {
instantiateModuleFn(
moduleId,
SourceType.Update,
outdatedModuleParents.get(moduleId)
)
} catch (err) {
if (typeof errorHandler === 'function') {
try {
errorHandler(err, { moduleId, module: devModuleCache[moduleId] })
} catch (err2) {
reportError(err2)
reportError(err)
}
} else {
reportError(err)
}
}
}
}
/**
* Internal implementation that orchestrates the full HMR update flow:
* invalidation, disposal, and application of new modules.
*/
function applyInternal(
outdatedModules: Set<ModuleId>,
disposedModules: Iterable<ModuleId>,
newModuleFactories: Map<ModuleId, HotModuleFactoryFunction>,
moduleFactories: ModuleFactories,
devModuleCache: ModuleCache<HotModule>,
instantiateModuleFn: (
moduleId: ModuleId,
sourceType: SourceType,
sourceData: SourceData
) => HotModule,
applyModuleFactoryNameFn: (factory: HotModuleFactoryFunction) => void
) {
outdatedModules = applyInvalidatedModules(outdatedModules)
// Find self-accepted modules to re-instantiate
const outdatedSelfAcceptedModules =
computeOutdatedSelfAcceptedModules(outdatedModules)
// Run dispose handlers, save hot.data, clear caches
const { outdatedModuleParents } = disposePhase(
outdatedModules,
disposedModules
)
let error: any
function reportError(err: any) {
if (!error) error = err // Keep first error
}
applyPhase(
outdatedSelfAcceptedModules,
newModuleFactories,
outdatedModuleParents,
moduleFactories,
devModuleCache,
instantiateModuleFn,
applyModuleFactoryNameFn,
reportError
)
if (error) {
throw error
}
// Recursively apply any queued invalidations from new module execution
if (queuedInvalidatedModules.size > 0) {
applyInternal(
new Set(),
[],
new Map(),
moduleFactories,
devModuleCache,
instantiateModuleFn,
applyModuleFactoryNameFn
)
}
}
/**
* Main entry point for applying an ECMAScript merged update.
* This is called by both browser and Node.js runtimes with platform-specific callbacks.
*/
function applyEcmascriptMergedUpdateShared(options: {
added: Map<ModuleId, EcmascriptModuleEntry | undefined>
modified: Map<ModuleId, EcmascriptModuleEntry>
disposedModules: Iterable<ModuleId>
evalModuleEntry: (entry: EcmascriptModuleEntry) => HotModuleFactoryFunction
instantiateModule: (
moduleId: ModuleId,
sourceType: SourceType,
sourceData: SourceData
) => HotModule
applyModuleFactoryName: (factory: HotModuleFactoryFunction) => void
moduleFactories: ModuleFactories
devModuleCache: ModuleCache<HotModule>
}) {
const {
added,
modified,
disposedModules,
evalModuleEntry,
instantiateModule,
applyModuleFactoryName,
moduleFactories,
devModuleCache,
} = options
const { outdatedModules, newModuleFactories } = computeOutdatedModules(
added,
modified,
evalModuleEntry
)
applyInternal(
outdatedModules,
disposedModules,
newModuleFactories,
moduleFactories,
devModuleCache,
instantiateModule,
applyModuleFactoryName
)
}
@@ -76,6 +76,14 @@ type LoadWebAssemblyModule = (
type ModuleCache<M> = Record<ModuleId, M>
// TODO properly type values here
type ModuleFactories = Map<ModuleId, Function>
/**
* Constraint for module factory functions used in HMR.
* Platform-specific runtimes define their own ModuleFactory types that must satisfy this constraint.
*/
type ModuleFactoryFunction<
TModule extends Module,
TContext extends TurbopackBaseContext<TModule>,
> = (this: TContext, ...args: unknown[]) => unknown
/**
* This is an alternating, non-empty arrow of module factory functions and module ids
* `[id1, id2..., factory1, id3, factory2, id4, id5, factory3]`
@@ -11,6 +11,32 @@
type EsmNamespaceObject = Record<string, any>
/**
* Describes why a module was instantiated.
* Shared between browser and Node.js runtimes.
*/
enum SourceType {
/**
* The module was instantiated because it was included in an evaluated chunk's
* runtime.
* SourceData is a ChunkPath.
*/
Runtime = 0,
/**
* The module was instantiated because a parent module imported it.
* SourceData is a ModuleId.
*/
Parent = 1,
/**
* The module was instantiated because it was included in a chunk's hot module
* update.
* SourceData is an array of ModuleIds or undefined.
*/
Update = 2,
}
type SourceData = ChunkPath | ModuleId | ModuleId[] | undefined
// @ts-ignore Defined in `dev-base.ts`
declare function getOrInstantiateModuleFromParent<M>(
id: ModuleId,
@@ -105,6 +131,17 @@ function createModuleObject(id: ModuleId): Module {
}
}
function createModuleWithDirection(id: ModuleId): ModuleWithDirection {
return {
exports: {},
error: undefined,
id,
namespaceObject: undefined,
parents: [],
children: [],
}
}
type BindingTag = 0
const BindingTag_Value = 0 as BindingTag
@@ -512,9 +549,21 @@ function installCompressedModuleFactories(
if (end === chunkModules.length) {
throw new Error('malformed chunk format, expected a factory function')
}
// Each chunk item has a 'primary id' and optional additional ids. If the primary id is already
// present we know all the additional ids are also present, so we don't need to check.
if (!moduleFactories.has(moduleId)) {
// Check if ANY of the module IDs in this group already have factories (e.g., from HMR updates).
// If so, skip installing the old factory from disk to preserve the HMR-updated code.
let hasExistingFactory = false
const groupIds: ModuleId[] = []
for (let j = i; j < end; j++) {
const id = chunkModules[j] as ModuleId
groupIds.push(id)
if (moduleFactories.has(id)) {
hasExistingFactory = true
break
}
}
if (!hasExistingFactory) {
const moduleFactoryFn = chunkModules[end] as Function
applyModuleFactoryName(moduleFactoryFn)
newModuleId?.(moduleId)
@@ -38,6 +38,7 @@ pub async fn get_browser_runtime_code(
match runtime_type {
RuntimeType::Production => runtime_base_code.push("browser/runtime/base/build-base.ts"),
RuntimeType::Development => {
runtime_base_code.push("shared/runtime/hmr-runtime.ts");
runtime_base_code.push("browser/runtime/base/dev-base.ts");
}
#[cfg(feature = "test")]
@@ -8,6 +8,26 @@ const WORKER_FORWARDED_GLOBALS = [];
*
* It will be prepended to the runtime code of each runtime.
*/ /* eslint-disable @typescript-eslint/no-unused-vars */ /// <reference path="./runtime-types.d.ts" />
/**
* Describes why a module was instantiated.
* Shared between browser and Node.js runtimes.
*/ var SourceType = /*#__PURE__*/ function(SourceType) {
/**
* The module was instantiated because it was included in an evaluated chunk's
* runtime.
* SourceData is a ChunkPath.
*/ SourceType[SourceType["Runtime"] = 0] = "Runtime";
/**
* The module was instantiated because a parent module imported it.
* SourceData is a ModuleId.
*/ SourceType[SourceType["Parent"] = 1] = "Parent";
/**
* The module was instantiated because it was included in a chunk's hot module
* update.
* SourceData is an array of ModuleIds or undefined.
*/ SourceType[SourceType["Update"] = 2] = "Update";
return SourceType;
}(SourceType || {});
const REEXPORTED_OBJECTS = new WeakMap();
/**
* Constructs the `__turbopack_context__` object for a module.
@@ -48,6 +68,16 @@ function getOverwrittenModule(moduleCache, id) {
namespaceObject: undefined
};
}
function createModuleWithDirection(id) {
return {
exports: {},
error: undefined,
id,
namespaceObject: undefined,
parents: [],
children: []
};
}
const BindingTag_Value = 0;
/**
* Adds the getters to the exports object.
@@ -341,9 +371,19 @@ function installCompressedModuleFactories(chunkModules, offset, moduleFactories,
if (end === chunkModules.length) {
throw new Error('malformed chunk format, expected a factory function');
}
// Each chunk item has a 'primary id' and optional additional ids. If the primary id is already
// present we know all the additional ids are also present, so we don't need to check.
if (!moduleFactories.has(moduleId)) {
// Check if ANY of the module IDs in this group already have factories (e.g., from HMR updates).
// If so, skip installing the old factory from disk to preserve the HMR-updated code.
let hasExistingFactory = false;
const groupIds = [];
for(let j = i; j < end; j++){
const id = chunkModules[j];
groupIds.push(id);
if (moduleFactories.has(id)) {
hasExistingFactory = true;
break;
}
}
if (!hasExistingFactory) {
const moduleFactoryFn = chunkModules[end];
applyModuleFactoryName(moduleFactoryFn);
newModuleId?.(moduleId);
File diff suppressed because one or more lines are too long
@@ -8,6 +8,26 @@ const WORKER_FORWARDED_GLOBALS = [];
*
* It will be prepended to the runtime code of each runtime.
*/ /* eslint-disable @typescript-eslint/no-unused-vars */ /// <reference path="./runtime-types.d.ts" />
/**
* Describes why a module was instantiated.
* Shared between browser and Node.js runtimes.
*/ var SourceType = /*#__PURE__*/ function(SourceType) {
/**
* The module was instantiated because it was included in an evaluated chunk's
* runtime.
* SourceData is a ChunkPath.
*/ SourceType[SourceType["Runtime"] = 0] = "Runtime";
/**
* The module was instantiated because a parent module imported it.
* SourceData is a ModuleId.
*/ SourceType[SourceType["Parent"] = 1] = "Parent";
/**
* The module was instantiated because it was included in a chunk's hot module
* update.
* SourceData is an array of ModuleIds or undefined.
*/ SourceType[SourceType["Update"] = 2] = "Update";
return SourceType;
}(SourceType || {});
const REEXPORTED_OBJECTS = new WeakMap();
/**
* Constructs the `__turbopack_context__` object for a module.
@@ -48,6 +68,16 @@ function getOverwrittenModule(moduleCache, id) {
namespaceObject: undefined
};
}
function createModuleWithDirection(id) {
return {
exports: {},
error: undefined,
id,
namespaceObject: undefined,
parents: [],
children: []
};
}
const BindingTag_Value = 0;
/**
* Adds the getters to the exports object.
@@ -341,9 +371,19 @@ function installCompressedModuleFactories(chunkModules, offset, moduleFactories,
if (end === chunkModules.length) {
throw new Error('malformed chunk format, expected a factory function');
}
// Each chunk item has a 'primary id' and optional additional ids. If the primary id is already
// present we know all the additional ids are also present, so we don't need to check.
if (!moduleFactories.has(moduleId)) {
// Check if ANY of the module IDs in this group already have factories (e.g., from HMR updates).
// If so, skip installing the old factory from disk to preserve the HMR-updated code.
let hasExistingFactory = false;
const groupIds = [];
for(let j = i; j < end; j++){
const id = chunkModules[j];
groupIds.push(id);
if (moduleFactories.has(id)) {
hasExistingFactory = true;
break;
}
}
if (!hasExistingFactory) {
const moduleFactoryFn = chunkModules[end];
applyModuleFactoryName(moduleFactoryFn);
newModuleId?.(moduleId);
@@ -163,6 +163,26 @@ function _ts_generator(thisArg, body) {
};
}
}
/**
* Describes why a module was instantiated.
* Shared between browser and Node.js runtimes.
*/ var SourceType = /*#__PURE__*/ function(SourceType) {
/**
* The module was instantiated because it was included in an evaluated chunk's
* runtime.
* SourceData is a ChunkPath.
*/ SourceType[SourceType["Runtime"] = 0] = "Runtime";
/**
* The module was instantiated because a parent module imported it.
* SourceData is a ModuleId.
*/ SourceType[SourceType["Parent"] = 1] = "Parent";
/**
* The module was instantiated because it was included in a chunk's hot module
* update.
* SourceData is an array of ModuleIds or undefined.
*/ SourceType[SourceType["Update"] = 2] = "Update";
return SourceType;
}(SourceType || {});
var REEXPORTED_OBJECTS = new WeakMap();
/**
* Constructs the `__turbopack_context__` object for a module.
@@ -203,6 +223,16 @@ function getOverwrittenModule(moduleCache, id) {
namespaceObject: undefined
};
}
function createModuleWithDirection(id) {
return {
exports: {},
error: undefined,
id: id,
namespaceObject: undefined,
parents: [],
children: []
};
}
var BindingTag_Value = 0;
/**
* Adds the getters to the exports object.
@@ -588,9 +618,19 @@ function installCompressedModuleFactories(chunkModules, offset, moduleFactories,
if (end === chunkModules.length) {
throw new Error('malformed chunk format, expected a factory function');
}
// Each chunk item has a 'primary id' and optional additional ids. If the primary id is already
// present we know all the additional ids are also present, so we don't need to check.
if (!moduleFactories.has(moduleId)) {
// Check if ANY of the module IDs in this group already have factories (e.g., from HMR updates).
// If so, skip installing the old factory from disk to preserve the HMR-updated code.
var hasExistingFactory = false;
var groupIds = [];
for(var j = i; j < end; j++){
var id = chunkModules[j];
groupIds.push(id);
if (moduleFactories.has(id)) {
hasExistingFactory = true;
break;
}
}
if (!hasExistingFactory) {
var moduleFactoryFn = chunkModules[end];
applyModuleFactoryName(moduleFactoryFn);
newModuleId === null || newModuleId === void 0 ? void 0 : newModuleId(moduleId);
@@ -951,23 +991,6 @@ function _ts_generator(thisArg, body) {
}
}
var browserContextPrototype = Context.prototype;
var SourceType = /*#__PURE__*/ function(SourceType) {
/**
* The module was instantiated because it was included in an evaluated chunk's
* runtime.
* SourceData is a ChunkPath.
*/ SourceType[SourceType["Runtime"] = 0] = "Runtime";
/**
* The module was instantiated because a parent module imported it.
* SourceData is a ModuleId.
*/ SourceType[SourceType["Parent"] = 1] = "Parent";
/**
* The module was instantiated because it was included in a chunk's hot module
* update.
* SourceData is an array of ModuleIds or undefined.
*/ SourceType[SourceType["Update"] = 2] = "Update";
return SourceType;
}(SourceType || {});
var moduleFactories = new Map();
contextPrototype.M = moduleFactories;
var availableModules = new Map();
@@ -975,13 +998,13 @@ var availableModuleChunks = new Map();
function factoryNotAvailableMessage(moduleId, sourceType, sourceData) {
var instantiationReason;
switch(sourceType){
case 0:
case SourceType.Runtime:
instantiationReason = `as a runtime entry of chunk ${sourceData}`;
break;
case 1:
case SourceType.Parent:
instantiationReason = `because it was required from module ${sourceData}`;
break;
case 2:
case SourceType.Update:
instantiationReason = 'because of an HMR update';
break;
default:
@@ -992,11 +1015,11 @@ function factoryNotAvailableMessage(moduleId, sourceType, sourceData) {
return `Module ${moduleId} was instantiated ${instantiationReason}, but the module factory is not available.`;
}
function loadChunk(chunkData) {
return loadChunkInternal(1, this.m.id, chunkData);
return loadChunkInternal(SourceType.Parent, this.m.id, chunkData);
}
browserContextPrototype.l = loadChunk;
function loadInitialChunk(chunkPath, chunkData) {
return loadChunkInternal(0, chunkPath, chunkData);
return loadChunkInternal(SourceType.Runtime, chunkPath, chunkData);
}
function loadChunkInternal(sourceType, sourceData, chunkData) {
return _async_to_generator(function() {
@@ -1177,7 +1200,7 @@ var loadedChunk = Promise.resolve(undefined);
var instrumentedBackendLoadChunks = new WeakMap();
// Do not make this async. React relies on referential equality of the returned Promise.
function loadChunkByUrl(chunkUrl) {
return loadChunkByUrlInternal(1, this.m.id, chunkUrl);
return loadChunkByUrlInternal(SourceType.Parent, this.m.id, chunkUrl);
}
browserContextPrototype.L = loadChunkByUrl;
// Do not make this async. React relies on referential equality of the returned Promise.
@@ -1189,13 +1212,13 @@ function loadChunkByUrlInternal(sourceType, sourceData, chunkUrl) {
entry = thenable.then(resolve).catch(function(cause) {
var loadReason;
switch(sourceType){
case 0:
case SourceType.Runtime:
loadReason = `as a runtime dependency of chunk ${sourceData}`;
break;
case 1:
case SourceType.Parent:
loadReason = `from module ${sourceData}`;
break;
case 2:
case SourceType.Update:
loadReason = 'from an HMR update';
break;
default:
@@ -1299,7 +1322,7 @@ browserContextPrototype.b = createWorker;
/**
* Instantiates a runtime module.
*/ function instantiateRuntimeModule(moduleId, chunkPath) {
return instantiateModule(moduleId, 0, chunkPath);
return instantiateModule(moduleId, SourceType.Runtime, chunkPath);
}
/**
* Returns the URL relative to the origin where a chunk can be fetched from.
@@ -1359,11 +1382,11 @@ var regexCssUrl = /\.css(?:\?[^#]*)?(?:#.*)?$/;
return regexCssUrl.test(chunkUrl);
}
function loadWebAssembly(chunkPath, edgeModule, importsObj) {
return BACKEND.loadWebAssembly(1, this.m.id, chunkPath, edgeModule, importsObj);
return BACKEND.loadWebAssembly(SourceType.Parent, this.m.id, chunkPath, edgeModule, importsObj);
}
contextPrototype.w = loadWebAssembly;
function loadWebAssemblyModule(chunkPath, edgeModule) {
return BACKEND.loadWebAssemblyModule(1, this.m.id, chunkPath, edgeModule);
return BACKEND.loadWebAssemblyModule(SourceType.Parent, this.m.id, chunkPath, edgeModule);
}
contextPrototype.u = loadWebAssemblyModule;
/// <reference path="./runtime-base.ts" />