From 93353e10d56dd1f49ecb63da1a6ffd26495e6b51 Mon Sep 17 00:00:00 2001 From: fireairforce <1344492820@qq.com> Date: Thu, 23 Jul 2026 11:14:55 +0800 Subject: [PATCH] perf(turbopack): add opt-in dynamic HMR chunk lists --- .../turbopack-browser/src/chunking_context.rs | 91 ++++++-- .../src/ecmascript/list/content.rs | 24 ++- .../src/browser/dev/hmr-client/hmr-client.ts | 194 ++++++++++++++++-- .../js/src/browser/runtime/base/dev-base.ts | 168 ++++++++++++++- .../runtime/dom/runtime-backend-dom.ts | 5 + .../js/src/shared/runtime/dev-globals.d.ts | 11 +- .../js/src/shared/runtime/dev-protocol.d.ts | 3 + 7 files changed, 449 insertions(+), 47 deletions(-) diff --git a/turbopack/crates/turbopack-browser/src/chunking_context.rs b/turbopack/crates/turbopack-browser/src/chunking_context.rs index b1bc51782bf8..feed0920d40d 100644 --- a/turbopack/crates/turbopack-browser/src/chunking_context.rs +++ b/turbopack/crates/turbopack-browser/src/chunking_context.rs @@ -77,6 +77,14 @@ impl BrowserChunkingContextBuilder { self } + /// Generate a separate HMR chunk list for each dynamic chunk group instead of recursively + /// adding every reachable dynamic chunk to the entry chunk list. The HMR transport must honor + /// the subscription's expected version and echo its validation token before use. + pub fn dynamic_hmr_chunk_lists(mut self) -> Self { + self.chunking_context.enable_dynamic_hmr_chunk_lists = true; + self + } + pub fn source_map_source_type(mut self, source_map_source_type: SourceMapSourceType) -> Self { self.chunking_context.source_map_source_type = source_map_source_type; self @@ -357,6 +365,8 @@ pub struct BrowserChunkingContext { default_url_behavior: Option, /// Enable HMR for this chunking enable_hot_module_replacement: bool, + /// Track dynamic chunk groups in their own HMR chunk lists instead of the entry chunk list. + enable_dynamic_hmr_chunk_lists: bool, /// Enable nested async availability for this chunking enable_nested_async_availability: bool, /// Enable module merging @@ -454,6 +464,7 @@ impl BrowserChunkingContext { url_behaviors: Default::default(), default_url_behavior: None, enable_hot_module_replacement: false, + enable_dynamic_hmr_chunk_lists: false, enable_nested_async_availability: false, enable_module_merging: false, enable_dynamic_chunk_content_loading: false, @@ -933,6 +944,8 @@ impl ChunkingContext for BrowserChunkingContext { ) -> Result> { let span = tracing::info_span!("chunking", name = display(ident.to_string().await?)); async move { + let this = self.await?; + let is_async_chunk_group = matches!(&chunk_group, ChunkGroup::Async(_)); let input_availability_info = availability_info; let MakeChunkGroupResult { chunks, @@ -948,12 +961,43 @@ impl ChunkingContext for BrowserChunkingContext { let chunks = chunks.await?; - let assets = chunks + let mut assets = chunks .iter() .map(|chunk| self.generate_chunk(*chunk)) .try_join() .await?; + if this.enable_hot_module_replacement + && this.enable_dynamic_hmr_chunk_lists + && is_async_chunk_group + { + let ident = if let Some(input_availability_info_ident) = + input_availability_info.ident().await? + { + ident + .owned() + .await? + .with_modifier(input_availability_info_ident) + .into_vc() + } else { + ident + }; + let other_assets = Vc::cell(assets); + let chunk_list = self + .generate_chunk_list_register_chunk( + ident, + EvaluatableAssets::empty(), + other_assets, + EcmascriptDevChunkListSource::Dynamic, + ) + .to_resolved() + .await?; + + // The list is the bootstrap for the dynamic group. The browser runtime first + // validates its HMR baseline, then loads the member chunks. + assets = vec![chunk_list]; + } + Ok(ChunkGroupResult { assets: ResolvedVc::cell(assets), referenced_assets: OutputAssets::empty_resolved(), @@ -1021,26 +1065,33 @@ impl ChunkingContext for BrowserChunkingContext { ); if this.enable_hot_module_replacement { - // Follow references (async loaders) to get actual dynamic component chunks, so - // the single HMR chunk list covers all lazily-loaded modules. - // inner=false: we only follow Reference inputs transitively, not Asset inputs, - // to avoid pulling in source maps and other asset-adjacent files that can't be - // reloaded by the DOM backend (which only handles CSS chunks via reloadChunk). - let all_dynamic_chunks = expand_output_assets( - references - .iter() - .copied() - .map(ExpandOutputAssetsInput::Reference) - .chain(assets.iter().copied().map(ExpandOutputAssetsInput::Asset)), - false, - ) - .await?; - - // Combine direct chunks, transitively-reachable dynamic chunks, and any caller- - // provided extras (e.g. RSC client reference chunks built outside this graph). let extra_chunks_ref = extra_chunks.await?; - let mut hmr_chunks: FxIndexSet>> = - all_dynamic_chunks.into_iter().collect(); + let mut hmr_chunks: FxIndexSet>> = if this + .enable_dynamic_hmr_chunk_lists + { + // Dynamic groups register their own lists when loaded, so the entry only + // tracks its directly-generated chunks. + assets.iter().copied().collect() + } else { + // Follow references (async loaders) to get actual dynamic component chunks, + // so the single HMR chunk list covers all lazily-loaded modules. `inner=false` + // avoids source maps and other asset-adjacent files that the DOM backend can't + // reload. + expand_output_assets( + references + .iter() + .copied() + .map(ExpandOutputAssetsInput::Reference) + .chain(assets.iter().copied().map(ExpandOutputAssetsInput::Asset)), + false, + ) + .await? + .into_iter() + .collect() + }; + + // Caller-provided chunks (for example RSC client references) are built outside + // this graph and must always remain in the entry list. hmr_chunks.extend(extra_chunks_ref.iter().copied()); let hmr_other_assets = Vc::cell(hmr_chunks.into_iter().collect()); diff --git a/turbopack/crates/turbopack-browser/src/ecmascript/list/content.rs b/turbopack/crates/turbopack-browser/src/ecmascript/list/content.rs index 414ac5dbf8eb..7cf5f6731c11 100644 --- a/turbopack/crates/turbopack-browser/src/ecmascript/list/content.rs +++ b/turbopack/crates/turbopack-browser/src/ecmascript/list/content.rs @@ -160,6 +160,27 @@ impl EcmascriptDevChunkListContent { // variable. Similarly, we register the chunk list with the // `{chunk_loading_global}_CHUNK_LISTS` global variable. let chunk_lists_global = format!("{}_CHUNK_LISTS", this.chunk_loading_global); + let dynamic_metadata = match this.source { + EcmascriptDevChunkListSource::Entry => String::new(), + EcmascriptDevChunkListSource::Dynamic => { + let version = self.version().id().owned().await?; + let chunk_versions: FxIndexMap<&str, RcStr> = this + .chunks_contents + .iter() + .map(async |(path, content)| { + Ok((path.as_str(), content.version().id().owned().await?)) + }) + .try_join() + .await? + .into_iter() + .collect(); + format!( + ",\nchunkVersions: {},\nversion: {}", + StringifyJs(&chunk_versions), + StringifyJs(&version) + ) + } + }; writedoc!( code, // `||=` would be better but we need to be es2020 compatible @@ -168,11 +189,12 @@ impl EcmascriptDevChunkListContent { (globalThis[{chunk_lists_global}] || (globalThis[{chunk_lists_global}] = [])).push({{ script: {script_or_path}, chunks: {chunks}, - source: {source} + source: {source}{dynamic_metadata} }}); "#, chunk_lists_global = StringifyJs(&chunk_lists_global), chunks = StringifyJs(&chunks), + dynamic_metadata = dynamic_metadata, source = StringifyJs(&this.source), )?; diff --git a/turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/dev/hmr-client/hmr-client.ts b/turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/dev/hmr-client/hmr-client.ts index 699af948c792..44c4ba92a972 100644 --- a/turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/dev/hmr-client/hmr-client.ts +++ b/turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/dev/hmr-client/hmr-client.ts @@ -62,27 +62,52 @@ export function connect({ const global = globalThis as unknown as Record< string, - ChunkUpdateProvider | [ChunkListPath, UpdateCallback][] | undefined + ChunkUpdateProvider | ChunkUpdateRegistration[] | undefined > const queued = global[chunkUpdateListenersGlobal] if (queued != null && !Array.isArray(queued)) { throw new Error('A separate HMR handler was already registered') } global[chunkUpdateListenersGlobal] = { - push: ([chunkPath, callback]: [ChunkListPath, UpdateCallback]) => { - subscribeToChunkUpdate(chunkPath, sendMessage, callback) + push: ([chunkPath, callback, options]: ChunkUpdateRegistration) => { + subscribeToChunkUpdate( + chunkPath, + sendMessage, + callback, + options?.conservative, + options?.onSubscribed, + options?.expectedVersion + ) }, } if (Array.isArray(queued)) { - for (const [chunkPath, callback] of queued) { - subscribeToChunkUpdate(chunkPath, sendMessage, callback) + for (const [chunkPath, callback, options] of queued) { + subscribeToChunkUpdate( + chunkPath, + sendMessage, + callback, + options?.conservative, + options?.onSubscribed, + options?.expectedVersion + ) } } } type UpdateCallbackSet = { callbacks: Set + conservative: boolean + onSubscribedCallbacks: Set<(revalidate: () => Promise) => void> + subscribed: boolean + expectedVersion?: string + validationToken?: string + validation?: { + promise: Promise + resolve: () => void + reject: (error: Error) => void + } + revalidate: () => Promise unsubscribe: () => void } @@ -101,13 +126,21 @@ function resourceKey(resource: ResourceIdentifier): ResourceKey { }) } +function createValidationToken() { + return `${Date.now()}:${Math.random()}` +} + function subscribeToUpdates( sendMessage: SendMessage, - resource: ResourceIdentifier + resource: ResourceIdentifier, + expectedVersion?: string, + validation?: string ): () => void { sendJSON(sendMessage, { type: 'turbopack-subscribe', ...resource, + version: expectedVersion, + validation, }) return () => { @@ -119,8 +152,17 @@ function subscribeToUpdates( } function handleSocketConnected(sendMessage: SendMessage) { - for (const key of updateCallbackSets.keys()) { - subscribeToUpdates(sendMessage, JSON.parse(key)) + for (const [key, callbackSet] of updateCallbackSets) { + callbackSet.subscribed = false + if (callbackSet.validationToken !== undefined) { + callbackSet.validationToken = createValidationToken() + } + callbackSet.unsubscribe = subscribeToUpdates( + sendMessage, + JSON.parse(key), + callbackSet.expectedVersion, + callbackSet.validationToken + ) } } @@ -144,11 +186,23 @@ function aggregateUpdates(msg: PartialServerMessage) { function applyAggregatedUpdates() { if (chunkListsWithPendingUpdates.size === 0) return + const updates = [...chunkListsWithPendingUpdates.values()] + chunkListsWithPendingUpdates.clear() + if ( + updates.length > 1 && + updates.some( + (update) => + updateCallbackSets.get(resourceKey(update.resource))?.conservative + ) + ) { + throw new Error( + 'Multiple dynamic HMR lists changed in one build; reloading conservatively.' + ) + } hooks.beforeRefresh() - for (const msg of chunkListsWithPendingUpdates.values()) { + for (const msg of updates) { triggerUpdate(msg) } - chunkListsWithPendingUpdates.clear() finalizeUpdate() } @@ -515,9 +569,26 @@ export function setHooks(newHooks: typeof hooks) { } function handleSocketMessage(msg: ServerMessage) { + const callbackSet = updateCallbackSets.get(resourceKey(msg.resource)) + if (!callbackSet || callbackSet.validationToken !== msg.validation) { + // Frames already queued by an old subscription can arrive after an + // unsubscribe. Only the active validation generation may update state. + return + } + sortIssues(msg.issues) - handleIssues(msg) + const hasCriticalIssues = handleIssues(msg) + if (!hasCriticalIssues && msg.type === 'issues') { + markUpdateSubscriptionReady(msg.resource) + } else if (msg.type !== 'issues') { + rejectUpdateSubscriptionValidation( + msg.resource, + new Error( + `Received ${msg.type} before the HMR subscription baseline was validated.` + ) + ) + } switch (msg.type) { case 'issues': @@ -537,6 +608,30 @@ function handleSocketMessage(msg: ServerMessage) { } } +function markUpdateSubscriptionReady(resource: ResourceIdentifier) { + const callbackSet = updateCallbackSets.get(resourceKey(resource)) + if (!callbackSet || callbackSet.subscribed) return + + callbackSet.subscribed = true + callbackSet.validation?.resolve() + callbackSet.validation = undefined + for (const callback of callbackSet.onSubscribedCallbacks) { + callback(callbackSet.revalidate) + } + callbackSet.onSubscribedCallbacks.clear() +} + +function rejectUpdateSubscriptionValidation( + resource: ResourceIdentifier, + error: Error +) { + const callbackSet = updateCallbackSets.get(resourceKey(resource)) + if (!callbackSet || callbackSet.subscribed) return + + callbackSet.validation?.reject(error) + callbackSet.validation = undefined +} + function finalizeUpdate() { hooks.refresh() hooks.buildOk() @@ -553,42 +648,111 @@ function finalizeUpdate() { function subscribeToChunkUpdate( chunkListPath: ChunkListPath, sendMessage: SendMessage, - callback: UpdateCallback + callback: UpdateCallback, + conservative?: boolean, + onSubscribed?: (revalidate: () => Promise) => void, + expectedVersion?: string ): () => void { return subscribeToUpdate( { path: chunkListPath, }, sendMessage, - callback + callback, + conservative, + onSubscribed, + expectedVersion ) } export function subscribeToUpdate( resource: ResourceIdentifier, sendMessage: SendMessage, - callback: UpdateCallback + callback: UpdateCallback, + conservative?: boolean, + onSubscribed?: (revalidate: () => Promise) => void, + expectedVersion?: string ) { const key = resourceKey(resource) let callbackSet: UpdateCallbackSet const existingCallbackSet = updateCallbackSets.get(key) if (!existingCallbackSet) { + let callbackSetRef!: UpdateCallbackSet + const revalidate = () => { + if (callbackSetRef.validation) return callbackSetRef.validation.promise + if (updateCallbackSets.get(key) !== callbackSetRef) { + return Promise.reject( + new Error( + `Cannot validate an inactive HMR subscription for ${resource.path}.` + ) + ) + } + + callbackSetRef.unsubscribe() + callbackSetRef.subscribed = false + callbackSetRef.validationToken = createValidationToken() + let resolve!: () => void + let reject!: (error: Error) => void + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve + reject = innerReject + }) + callbackSetRef.validation = { promise, resolve, reject } + callbackSetRef.unsubscribe = subscribeToUpdates( + sendMessage, + resource, + expectedVersion, + callbackSetRef.validationToken + ) + return promise + } + const validationToken = onSubscribed ? createValidationToken() : undefined callbackSet = { callbacks: new Set([callback]), - unsubscribe: subscribeToUpdates(sendMessage, resource), + conservative: Boolean(conservative), + onSubscribedCallbacks: new Set(onSubscribed ? [onSubscribed] : []), + subscribed: false, + expectedVersion, + validationToken, + revalidate, + unsubscribe: subscribeToUpdates( + sendMessage, + resource, + expectedVersion, + validationToken + ), } + callbackSetRef = callbackSet updateCallbackSets.set(key, callbackSet) } else { + if (existingCallbackSet.expectedVersion !== expectedVersion) { + location.reload() + return () => {} + } existingCallbackSet.callbacks.add(callback) + existingCallbackSet.conservative ||= Boolean(conservative) + if (onSubscribed) { + if (existingCallbackSet.subscribed) { + onSubscribed(existingCallbackSet.revalidate) + } else { + existingCallbackSet.onSubscribedCallbacks.add(onSubscribed) + } + } callbackSet = existingCallbackSet } return () => { callbackSet.callbacks.delete(callback) + if (onSubscribed) callbackSet.onSubscribedCallbacks.delete(onSubscribed) if (callbackSet.callbacks.size === 0) { callbackSet.unsubscribe() updateCallbackSets.delete(key) + callbackSet.validationToken = undefined + callbackSet.validation?.reject( + new Error(`HMR subscription for ${resource.path} was removed.`) + ) + callbackSet.validation = undefined } } } diff --git a/turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/runtime/base/dev-base.ts b/turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/runtime/base/dev-base.ts index 3dead3461737..f582f2e51a5c 100644 --- a/turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/runtime/base/dev-base.ts +++ b/turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/runtime/base/dev-base.ts @@ -66,6 +66,14 @@ interface DevRuntimeBackend { restart: () => void } +type DevChunkList = + | (Omit & { source: 'entry' }) + | (Omit & { + source: 'dynamic' + chunkVersions: Record + version: string + }) + /** * Map from module ID to the chunks that contain this module. * @@ -92,6 +100,10 @@ const chunkListChunksMap: Map> = new Map() * Map from a chunk path to the chunk lists it belongs to. */ const chunkChunkListsMap: Map> = new Map() +/** Dynamic lists use a stricter, reload-on-ambiguity update policy. */ +const dynamicChunkLists: Set = new Set() +/** Dynamic manifests which have not finished their subscribe/load/revalidate handshake. */ +const loadingDynamicChunkLists: Set = new Set() /** * Gets or instantiates a runtime module. @@ -408,6 +420,31 @@ function handleApply(chunkListPath: ChunkListPath, update: ServerMessage) { switch (update.type) { case 'partial': { // This indicates that the update is can be applied to the current state of the application. + if (dynamicChunkLists.has(chunkListPath)) { + if (loadingDynamicChunkLists.has(chunkListPath)) { + throw new Error( + `Dynamic chunk list ${chunkListPath} changed while it was loading.` + ) + } + if ( + update.instruction.type === 'ChunkListUpdate' && + update.instruction.chunks != null + ) { + throw new Error( + `Dynamic chunk list ${chunkListPath} changed structure.` + ) + } + const hasSharedChunk = [ + ...(chunkListChunksMap.get(chunkListPath) ?? []), + ].some( + (chunkPath) => (chunkChunkListsMap.get(chunkPath)?.size ?? 0) > 1 + ) + if (hasSharedChunk) { + throw new Error( + `Dynamic chunk list ${chunkListPath} shares chunks with another active list.` + ) + } + } applyUpdate(update.instruction) break } @@ -423,7 +460,10 @@ function handleApply(chunkListPath: ChunkListPath, update: ServerMessage) { // or the page itself was deleted. // If it is a dynamic import, we simply discard all modules that the chunk has exclusive access to. // If it is a runtime chunk list, we restart the application. - if (runtimeChunkLists.has(chunkListPath)) { + if ( + runtimeChunkLists.has(chunkListPath) || + dynamicChunkLists.has(chunkListPath) + ) { DEV_BACKEND.restart() } else { disposeChunkList(chunkListPath) @@ -553,8 +593,26 @@ function markChunkListAsRuntime(chunkListPath: ChunkListPath) { runtimeChunkLists.add(chunkListPath) } +function getDevChunkFromRegistration( + chunk: ChunkRegistrationChunk +): ChunkPath | CurrentScript { + if (typeof chunk === 'string') { + if (typeof document !== 'undefined') { + const src = document.currentScript?.getAttribute('src') + if ( + src && + /[?&]hmr=/.test(src) && + getPathFromScript({ src } as ChunkScript) === (chunk as ChunkPath) + ) { + return { src } as CurrentScript + } + } + } + return getChunkFromRegistration(chunk) +} + function registerChunk(registration: ChunkRegistration) { - const chunk = getChunkFromRegistration(registration[0]) as + const chunk = getDevChunkFromRegistration(registration[0]) as | ChunkPath | ChunkScript let runtimeParams: RuntimeParams | undefined @@ -574,20 +632,37 @@ function registerChunk(registration: ChunkRegistration) { return BACKEND.registerChunk(chunk, runtimeParams) } +function getVersionedChunkUrl( + chunkPath: ChunkPath, + versionToken: string +): ChunkUrl { + const chunkUrl = getChunkRelativeUrl(chunkPath) + const separator = chunkUrl.includes('?') ? '&' : '?' + return `${chunkUrl}${separator}hmr=${encodeURIComponent( + versionToken + )}` as ChunkUrl +} + +function loadVersionedInitialChunk( + chunkListPath: ChunkPath, + chunkData: ChunkData, + versionToken: string +) { + return loadChunkByUrlInternal( + SourceType.Runtime, + chunkListPath, + getVersionedChunkUrl(getChunkPath(chunkData), versionToken) + ) +} + /** * Subscribes to chunk list updates from the update server and applies them. */ -function registerChunkList(chunkList: ChunkList) { - const chunkListScript = getChunkFromRegistration(chunkList.script) as +function registerChunkList(chunkList: DevChunkList) { + const chunkListScript = getDevChunkFromRegistration(chunkList.script) as | ChunkListPath | ChunkListScript const chunkListPath = getPathFromScript(chunkListScript) - // The "chunk" is also registered to finish the loading in the backend - BACKEND.registerChunk(chunkListPath as string as ChunkPath) - CHUNK_UPDATE_LISTENERS.push([ - chunkListPath, - handleApply.bind(null, chunkListPath), - ]) // Adding chunks to chunk lists and vice versa. const chunkPaths = new Set(chunkList.chunks.map(getChunkPath)) @@ -603,6 +678,79 @@ function registerChunkList(chunkList: ChunkList) { } if (chunkList.source === 'entry') { + // Entry lists keep the legacy loading and update behavior. + BACKEND.registerChunk(chunkListPath as string as ChunkPath) + CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]) markChunkListAsRuntime(chunkListPath) + return } + + dynamicChunkLists.add(chunkListPath) + + if (typeof document === 'undefined') { + // Workers do not have the browser HMR transport. Load the manifest members + // directly so the dynamic import cannot wait for a subscription forever. + void Promise.all( + chunkList.chunks.map((chunkData) => + loadInitialChunk(chunkListPath as string as ChunkPath, chunkData) + ) + ).then( + () => BACKEND.registerChunk(chunkListPath as string as ChunkPath), + () => DEV_BACKEND.restart() + ) + return + } + + loadingDynamicChunkLists.add(chunkListPath) + let resolveSubscribed!: () => void + let revalidateSubscription!: () => Promise + const subscribed = new Promise((resolve) => { + resolveSubscribed = resolve + }) + + CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + { + conservative: true, + expectedVersion: chunkList.version, + onSubscribed: (revalidate) => { + revalidateSubscription = revalidate + resolveSubscribed() + }, + }, + ]) + + void (async () => { + try { + await subscribed + await Promise.all( + chunkList.chunks.map((chunkData) => { + const chunkPath = getChunkPath(chunkData) + const chunkVersion = chunkList.chunkVersions[chunkPath] + if (chunkVersion === undefined) { + throw new Error( + `Missing version for dynamic member chunk ${chunkPath}` + ) + } + return loadVersionedInitialChunk( + chunkListPath as string as ChunkPath, + chunkData, + chunkVersion + ) + }) + ) + // Member URLs are served from the live output directory. Verify that + // the manifest did not change while those HTTP responses were loading. + await revalidateSubscription() + loadingDynamicChunkLists.delete(chunkListPath) + BACKEND.registerChunk(chunkListPath as string as ChunkPath) + } catch { + loadingDynamicChunkLists.delete(chunkListPath) + DEV_BACKEND.restart() + } + })() } diff --git a/turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/runtime/dom/runtime-backend-dom.ts b/turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/runtime/dom/runtime-backend-dom.ts index 762af37ebf67..f67f23f09ad1 100644 --- a/turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/runtime/dom/runtime-backend-dom.ts +++ b/turbopack/crates/turbopack-ecmascript-runtime/js/src/browser/runtime/dom/runtime-backend-dom.ts @@ -42,6 +42,11 @@ const chunkResolvers: Map = new Map() const resolver = getOrCreateResolver(chunkUrl) resolver.resolve() + const exactChunkUrl = getUrlFromScript(chunk) + if (exactChunkUrl !== chunkUrl && /[?&]hmr=/.test(exactChunkUrl)) { + getOrCreateResolver(exactChunkUrl).resolve() + } + if (params == null) { return } diff --git a/turbopack/crates/turbopack-ecmascript-runtime/js/src/shared/runtime/dev-globals.d.ts b/turbopack/crates/turbopack-ecmascript-runtime/js/src/shared/runtime/dev-globals.d.ts index 4b4ebd1f26f0..570503b93dd0 100644 --- a/turbopack/crates/turbopack-ecmascript-runtime/js/src/shared/runtime/dev-globals.d.ts +++ b/turbopack/crates/turbopack-ecmascript-runtime/js/src/shared/runtime/dev-globals.d.ts @@ -6,9 +6,18 @@ */ type UpdateCallback = (update: ServerMessage) => void +type ChunkUpdateRegistration = [ + chunkListPath: ChunkListPath, + callback: UpdateCallback, + options?: { + conservative?: boolean + onSubscribed?: (revalidate: () => Promise) => void + expectedVersion?: string + }, +] type ChunkUpdateProvider = { - push: (registration: [ChunkListPath, UpdateCallback]) => void + push: (registration: ChunkUpdateRegistration) => void } declare var CHUNK_UPDATE_LISTENERS: ChunkUpdateProvider diff --git a/turbopack/crates/turbopack-ecmascript-runtime/js/src/shared/runtime/dev-protocol.d.ts b/turbopack/crates/turbopack-ecmascript-runtime/js/src/shared/runtime/dev-protocol.d.ts index 9e646322acd2..9154953766e0 100644 --- a/turbopack/crates/turbopack-ecmascript-runtime/js/src/shared/runtime/dev-protocol.d.ts +++ b/turbopack/crates/turbopack-ecmascript-runtime/js/src/shared/runtime/dev-protocol.d.ts @@ -15,6 +15,7 @@ type ModuleFactoryString = string type ServerMessage = { resource: ResourceIdentifier issues: Issue[] + validation?: string } & ( | { type: 'restart' @@ -98,6 +99,8 @@ type ResourceIdentifier = { type ClientMessageSubscribe = { type: 'turbopack-subscribe' + version?: string + validation?: string } & ResourceIdentifier type ClientMessageUnsubscribe = {