diff --git a/crates/next-core/js/src/dev/hmr-client.ts b/crates/next-core/js/src/dev/hmr-client.ts index 42853880ce32c..4654a1af2acf7 100644 --- a/crates/next-core/js/src/dev/hmr-client.ts +++ b/crates/next-core/js/src/dev/hmr-client.ts @@ -1,6 +1,10 @@ import type { + ChunkListUpdate, + ChunkUpdate, ClientMessage, - HmrUpdateEntry, + EcmascriptMergedChunkUpdate, + EcmascriptMergedUpdate, + EcmascriptModuleEntry, Issue, ResourceIdentifier, ServerMessage, @@ -56,8 +60,6 @@ export function connect({ assetPrefix }: ClientOptions) { subscribeToChunkUpdate(chunkPath, callback); } } - - subscribeToInitialCssChunksUpdates(assetPrefix); } type UpdateCallbackSet = { @@ -100,116 +102,334 @@ function handleSocketConnected() { } } -type AggregatedUpdates = { - added: Record; - modified: Record; - deleted: Set; -}; - -// we aggregate all updates until the issues are resolved -const chunksWithUpdates: Map = new Map(); +// we aggregate all pending updates until the issues are resolved +const chunkListsWithPendingUpdates: Map< + ResourceKey, + { update: ChunkListUpdate; resource: ResourceIdentifier } +> = new Map(); function aggregateUpdates( msg: ServerMessage, - hasCriticalIssues: boolean + aggregate: boolean ): ServerMessage { const key = resourceKey(msg.resource); - const aggregated = chunksWithUpdates.get(key); - - if (msg.type === "issues" && aggregated == null && hasCriticalIssues) { - // add an empty record to make sure we don't call `onBuildOk` - chunksWithUpdates.set(key, { - added: {}, - modified: {}, - deleted: new Set(), - }); - } + let aggregated = chunkListsWithPendingUpdates.get(key); if (msg.type === "issues" && aggregated != null) { - if (!hasCriticalIssues) { - chunksWithUpdates.delete(key); + if (!aggregate) { + chunkListsWithPendingUpdates.delete(key); } return { ...msg, type: "partial", - instruction: { - type: "EcmascriptChunkUpdate", - added: aggregated.added, - modified: aggregated.modified, - deleted: Array.from(aggregated.deleted), - }, + instruction: aggregated.update, }; } if (msg.type !== "partial") return msg; if (aggregated == null) { - if (hasCriticalIssues) { - chunksWithUpdates.set(key, { - added: msg.instruction.added, - modified: msg.instruction.modified, - deleted: new Set(msg.instruction.deleted), + if (aggregate) { + chunkListsWithPendingUpdates.set(key, { + resource: msg.resource, + update: msg.instruction, }); } return msg; } - for (const [moduleId, entry] of Object.entries(msg.instruction.added)) { - const removedDeleted = aggregated.deleted.delete(moduleId); - if (aggregated.modified[moduleId] != null) { - console.error( - `impossible state aggregating updates: module "${moduleId}" was added, but previously modified` - ); - location.reload(); + aggregated = { + resource: msg.resource, + update: mergeChunkListUpdates(aggregated.update, msg.instruction), + }; + + if (aggregate) { + chunkListsWithPendingUpdates.set(key, aggregated); + } else { + // Once we receive a partial update with no critical issues, we can stop aggregating updates. + // The aggregated update will be applied. + chunkListsWithPendingUpdates.delete(key); + } + + return { + ...msg, + instruction: aggregated.update, + }; +} + +function mergeChunkListUpdates( + updateA: ChunkListUpdate, + updateB: ChunkListUpdate +): ChunkListUpdate { + let chunks; + if (updateA.chunks != null) { + if (updateB.chunks == null) { + chunks = updateA.chunks; + } else { + chunks = mergeChunkListChunks(updateA.chunks, updateB.chunks); } + } else if (updateB.chunks != null) { + chunks = updateB.chunks; + } - if (removedDeleted) { - aggregated.modified[moduleId] = entry; + let merged; + if (updateA.merged != null) { + if (updateB.merged == null) { + merged = updateA.merged; } else { - aggregated.added[moduleId] = entry; + // Since `merged` is an array of updates, we need to merge them all into + // one, consistent update. + // Since there can only be `EcmascriptMergeUpdates` in the array, there is + // no need to key on the `type` field. + let update = updateA.merged[0]; + for (let i = 1; i < updateA.merged.length; i++) { + update = mergeChunkListEcmascriptMergedUpdates( + update, + updateA.merged[i] + ); + } + + for (let i = 0; i < updateB.merged.length; i++) { + update = mergeChunkListEcmascriptMergedUpdates( + update, + updateB.merged[i] + ); + } + + merged = [update]; } + } else if (updateB.merged != null) { + merged = updateB.merged; } - for (const [moduleId, entry] of Object.entries(msg.instruction.modified)) { - if (aggregated.added[moduleId] != null) { - aggregated.added[moduleId] = entry; + return { + type: "ChunkListUpdate", + chunks, + merged, + }; +} + +function mergeChunkListChunks( + chunksA: Record, + chunksB: Record +): Record { + const chunks: Record = {}; + + for (const [chunkPath, chunkUpdateA] of Object.entries(chunksA)) { + const chunkUpdateB = chunksB[chunkPath]; + if (chunkUpdateB != null) { + const mergedUpdate = mergeChunkUpdates(chunkUpdateA, chunkUpdateB); + if (mergedUpdate != null) { + chunks[chunkPath] = mergedUpdate; + } } else { - aggregated.modified[moduleId] = entry; + chunks[chunkPath] = chunkUpdateA; } + } - if (aggregated.deleted.has(moduleId)) { - console.error( - `impossible state aggregating updates: module "${moduleId}" was modified, but previously deleted` - ); - location.reload(); + for (const [chunkPath, chunkUpdateB] of Object.entries(chunksB)) { + if (chunks[chunkPath] == null) { + chunks[chunkPath] = chunkUpdateB; } } - for (const moduleId of msg.instruction.deleted) { - delete aggregated.added[moduleId]; - delete aggregated.modified[moduleId]; - aggregated.deleted.add(moduleId); + return chunks; +} + +function mergeChunkUpdates( + updateA: ChunkUpdate, + updateB: ChunkUpdate +): ChunkUpdate | undefined { + if ( + (updateA.type === "added" && updateB.type === "deleted") || + (updateA.type === "deleted" && updateB.type === "added") + ) { + return undefined; } - if (!hasCriticalIssues) { - chunksWithUpdates.delete(key); - } else { - chunksWithUpdates.set(key, aggregated); + if (updateA.type === "partial") { + invariant(updateA.instruction, "Partial updates are unsupported"); + } + + if (updateB.type === "partial") { + invariant(updateB.instruction, "Partial updates are unsupported"); } + return undefined; +} + +function mergeChunkListEcmascriptMergedUpdates( + mergedA: EcmascriptMergedUpdate, + mergedB: EcmascriptMergedUpdate +): EcmascriptMergedUpdate { + const entries = mergeEcmascriptChunkEntries(mergedA.entries, mergedB.entries); + const chunks = mergeEcmascriptChunksUpdates(mergedA.chunks, mergedB.chunks); + return { - ...msg, - instruction: { - type: "EcmascriptChunkUpdate", - added: aggregated.added, - modified: aggregated.modified, - deleted: Array.from(aggregated.deleted), - }, + type: "EcmascriptMergedUpdate", + entries, + chunks, }; } +function mergeEcmascriptChunkEntries( + entriesA: Record | undefined, + entriesB: Record | undefined +): Record { + return { ...entriesA, ...entriesB }; +} + +function mergeEcmascriptChunksUpdates( + chunksA: Record | undefined, + chunksB: Record | undefined +): Record | undefined { + if (chunksA == null) { + return chunksB; + } + + if (chunksB == null) { + return chunksA; + } + + const chunks: Record = {}; + + for (const [chunkPath, chunkUpdateA] of Object.entries(chunksA)) { + const chunkUpdateB = chunksB[chunkPath]; + if (chunkUpdateB != null) { + const mergedUpdate = mergeEcmascriptChunkUpdates( + chunkUpdateA, + chunkUpdateB + ); + if (mergedUpdate != null) { + chunks[chunkPath] = mergedUpdate; + } + } else { + chunks[chunkPath] = chunkUpdateA; + } + } + + for (const [chunkPath, chunkUpdateB] of Object.entries(chunksB)) { + if (chunks[chunkPath] == null) { + chunks[chunkPath] = chunkUpdateB; + } + } + + if (Object.keys(chunks).length === 0) { + return undefined; + } + + return chunks; +} + +function mergeEcmascriptChunkUpdates( + updateA: EcmascriptMergedChunkUpdate, + updateB: EcmascriptMergedChunkUpdate +): EcmascriptMergedChunkUpdate | undefined { + if (updateA.type === "added" && updateB.type === "deleted") { + // These two completely cancel each other out. + return undefined; + } + + if (updateA.type === "deleted" && updateB.type === "added") { + const added = []; + const deleted = []; + const deletedModules = new Set(updateA.modules ?? []); + const addedModules = new Set(updateB.modules ?? []); + + for (const moduleId of addedModules) { + if (!deletedModules.has(moduleId)) { + added.push(moduleId); + } + } + + for (const moduleId of deletedModules) { + if (!addedModules.has(moduleId)) { + deleted.push(moduleId); + } + } + + if (added.length === 0 && deleted.length === 0) { + return undefined; + } + + return { + type: "partial", + added, + deleted, + }; + } + + if (updateA.type === "partial" && updateB.type === "partial") { + const added = new Set([...(updateA.added ?? []), ...(updateB.added ?? [])]); + const deleted = new Set([ + ...(updateA.deleted ?? []), + ...(updateB.deleted ?? []), + ]); + + if (updateB.added != null) { + for (const moduleId of updateB.added) { + deleted.delete(moduleId); + } + } + + if (updateB.deleted != null) { + for (const moduleId of updateB.deleted) { + added.delete(moduleId); + } + } + + return { + type: "partial", + added: [...added], + deleted: [...deleted], + }; + } + + if (updateA.type === "added" && updateB.type === "partial") { + const modules = new Set([ + ...(updateA.modules ?? []), + ...(updateB.added ?? []), + ]); + + for (const moduleId of updateB.deleted ?? []) { + modules.delete(moduleId); + } + + return { + type: "added", + modules: [...modules], + }; + } + + if (updateA.type === "partial" && updateB.type === "deleted") { + // We could eagerly return `updateB` here, but this would potentially be + // incorrect if `updateA` has added modules. + + const modules = new Set(updateB.modules ?? []); + + if (updateA.added != null) { + for (const moduleId of updateA.added) { + modules.delete(moduleId); + } + } + + return { + type: "deleted", + modules: [...modules], + }; + } + + // Any other update combination is invalid. + + return undefined; +} + +function invariant(never: never, message: string): never { + throw new Error(`Invariant: ${message}`); +} + const CRITICAL = ["bug", "error", "fatal"]; function compareByList(list: any[], a: any, b: any) { @@ -282,9 +502,14 @@ function handleSocketMessage(msg: ServerMessage) { sortIssues(msg.issues); const hasCriticalIssues = handleIssues(msg); - const aggregatedMsg = aggregateUpdates(msg, hasCriticalIssues); - const runHooks = chunksWithUpdates.size === 0; + // TODO(WEB-582) Disable update aggregation for now. + const aggregate = /* hasCriticalIssues */ false; + const aggregatedMsg = aggregateUpdates(msg, aggregate); + + if (aggregate) return; + + const runHooks = chunkListsWithPendingUpdates.size === 0; if (aggregatedMsg.type !== "issues") { if (runHooks) onBeforeRefresh(); @@ -354,6 +579,16 @@ function triggerUpdate(msg: ServerMessage) { for (const callback of callbackSet.callbacks) { callback(msg); } + + if (msg.type === "notFound") { + // This indicates that the resource which we subscribed to either does not exist or + // has been deleted. In either case, we should clear all update callbacks, so if a + // new subscription is created for the same resource, it will send a new "subscribe" + // message to the server. + // No need to send an "unsubscribe" message to the server, it will have already + // dropped the update stream before sending the "notFound" message. + updateCallbackSets.delete(key); + } } catch (err) { console.error( `An error occurred during the update of resource \`${msg.resource.path}\``, @@ -362,46 +597,3 @@ function triggerUpdate(msg: ServerMessage) { location.reload(); } } - -// Unlike ES chunks, CSS chunks cannot contain the logic to accept updates. -// They must be reloaded here instead. -function subscribeToInitialCssChunksUpdates(assetPrefix: string) { - const initialCssChunkLinks: NodeListOf = - document.head.querySelectorAll(`link[rel="stylesheet"]`); - - initialCssChunkLinks.forEach((link) => { - subscribeToCssChunkUpdates(assetPrefix, link); - }); -} - -export function subscribeToCssChunkUpdates( - assetPrefix: string, - link: HTMLLinkElement -) { - const cssChunkPrefix = `${assetPrefix}/`; - - const href = link.href; - if (href == null) { - return; - } - - const { pathname, origin } = new URL(href); - if (origin !== location.origin || !pathname.startsWith(cssChunkPrefix)) { - return; - } - - const chunkPath = pathname.slice(cssChunkPrefix.length); - subscribeToChunkUpdate(chunkPath, (update) => { - switch (update.type) { - case "restart": { - console.info(`Reloading CSS chunk \`${chunkPath}\``); - link.replaceWith(link); - break; - } - case "partial": - throw new Error(`partial CSS chunk updates are not supported`); - default: - throw new Error(`unknown update type \`${update}\``); - } - }); -} diff --git a/crates/next-core/js/src/entry/app-renderer.tsx b/crates/next-core/js/src/entry/app-renderer.tsx index ec135ce31efd0..ebb67f12f6cc1 100644 --- a/crates/next-core/js/src/entry/app-renderer.tsx +++ b/crates/next-core/js/src/entry/app-renderer.tsx @@ -37,7 +37,7 @@ import { headersFromEntries } from "@vercel/turbopack-next/internal/headers"; import { parse, ParsedUrlQuery } from "node:querystring"; globalThis.__next_require__ = (data) => { - const [, , ssr_id] = JSON.parse(data); + const [, , , ssr_id] = JSON.parse(data); return __turbopack_require__(ssr_id); }; globalThis.__next_chunk_load__ = () => Promise.resolve(); diff --git a/crates/next-core/js/src/entry/app/hydrate.tsx b/crates/next-core/js/src/entry/app/hydrate.tsx index 7e9eacb1dba10..2be819e8ff676 100644 --- a/crates/next-core/js/src/entry/app/hydrate.tsx +++ b/crates/next-core/js/src/entry/app/hydrate.tsx @@ -19,7 +19,8 @@ window.next = { }; globalThis.__next_require__ = (data) => { - const [client_id] = JSON.parse(data); + const [client_id, chunks, chunkListPath] = JSON.parse(data); + __turbopack_register_chunk_list__(chunkListPath, chunks); return __turbopack_require__(client_id); }; globalThis.__next_chunk_load__ = __turbopack_load__; diff --git a/crates/next-core/js/src/entry/app/index.d.ts b/crates/next-core/js/src/entry/app/index.d.ts index df1093e038400..69e3db64322ae 100644 --- a/crates/next-core/js/src/entry/app/index.d.ts +++ b/crates/next-core/js/src/entry/app/index.d.ts @@ -5,3 +5,4 @@ export = Anything; export const __turbopack_module_id__: string | number; export const chunks: string[]; +export const chunkListPath: string; diff --git a/crates/next-core/js/src/entry/app/server-to-client-ssr.tsx b/crates/next-core/js/src/entry/app/server-to-client-ssr.tsx index 8b7dfa3a36c2d..41e0ab50cf1c1 100644 --- a/crates/next-core/js/src/entry/app/server-to-client-ssr.tsx +++ b/crates/next-core/js/src/entry/app/server-to-client-ssr.tsx @@ -5,6 +5,8 @@ import { createProxy } from "next/dist/build/webpack/loaders/next-flight-loader/ import { __turbopack_module_id__ as id } from "CLIENT_MODULE"; // @ts-expect-error CLIENT_CHUNKS is provided by rust -import client_id, { chunks } from "CLIENT_CHUNKS"; +import client_id, { chunks, chunkListPath } from "CLIENT_CHUNKS"; -export default createProxy(JSON.stringify([client_id, chunks, id])); +export default createProxy( + JSON.stringify([client_id, chunks, chunkListPath, id]) +); diff --git a/crates/next-core/js/src/entry/app/server-to-client.tsx b/crates/next-core/js/src/entry/app/server-to-client.tsx index 30c569c4df514..142d9e32732df 100644 --- a/crates/next-core/js/src/entry/app/server-to-client.tsx +++ b/crates/next-core/js/src/entry/app/server-to-client.tsx @@ -1,6 +1,6 @@ import { createProxy } from "next/dist/build/webpack/loaders/next-flight-loader/module-proxy"; // @ts-expect-error CLIENT_CHUNKS is provided by rust -import client_id, { chunks } from "CLIENT_CHUNKS"; +import client_id, { chunks, chunkListPath } from "CLIENT_CHUNKS"; -export default createProxy(JSON.stringify([client_id, chunks])); +export default createProxy(JSON.stringify([client_id, chunks, chunkListPath])); diff --git a/crates/next-core/js/src/entry/fallback.tsx b/crates/next-core/js/src/entry/fallback.tsx index 0ce5f210ba903..5aefc76bd0ca4 100644 --- a/crates/next-core/js/src/entry/fallback.tsx +++ b/crates/next-core/js/src/entry/fallback.tsx @@ -18,7 +18,7 @@ subscribeToUpdate( }, }, (update) => { - if (update.type === "restart") { + if (update.type === "restart" || update.type === "notFound") { location.reload(); } } diff --git a/crates/next-core/js/src/entry/next-hydrate.tsx b/crates/next-core/js/src/entry/next-hydrate.tsx index b7fa676454533..3fd0b24361cfd 100644 --- a/crates/next-core/js/src/entry/next-hydrate.tsx +++ b/crates/next-core/js/src/entry/next-hydrate.tsx @@ -97,7 +97,7 @@ function subscribeToPageManifest({ assetPrefix }: { assetPrefix: string }) { path: "_next/static/development/_devPagesManifest.json", }, (update) => { - if (["restart", "partial"].includes(update.type)) { + if (["restart", "notFound", "partial"].includes(update.type)) { return; } diff --git a/crates/next-core/js/types/globals.d.ts b/crates/next-core/js/types/globals.d.ts index a89ab2815f8a9..d134a3e9714c7 100644 --- a/crates/next-core/js/types/globals.d.ts +++ b/crates/next-core/js/types/globals.d.ts @@ -1,6 +1,10 @@ declare global { function __turbopack_require__(name: any): any; function __turbopack_load__(path: string): any; + function __turbopack_register_chunk_list__( + chunkListPath: string, + chunksPaths: string[] + ): any; function __webpack_require__(name: any): any; var __webpack_public_path__: string | undefined; var __DEV_MIDDLEWARE_MATCHERS: any[]; diff --git a/crates/next-core/src/app_source.rs b/crates/next-core/src/app_source.rs index 409292cba2b58..33cae92b70365 100644 --- a/crates/next-core/src/app_source.rs +++ b/crates/next-core/src/app_source.rs @@ -411,7 +411,7 @@ async fn create_app_source_for_directory( segments: layouts, } => { let LayoutSegment { target, .. } = *segment.await?; - let pathname = pathname_for_path(server_root, url, false); + let pathname = pathname_for_path(server_root, url, false, false); let params_matcher = NextParamsMatcherVc::new(pathname); sources.push(create_node_rendered_source( @@ -443,7 +443,7 @@ async fn create_app_source_for_directory( route, .. } => { - let pathname = pathname_for_path(server_root, url, false); + let pathname = pathname_for_path(server_root, url, false, false); let params_matcher = NextParamsMatcherVc::new(pathname); sources.push(create_node_api_source( diff --git a/crates/next-core/src/next_client/transition.rs b/crates/next-core/src/next_client/transition.rs index 6d8d8441a1f7b..bf63b4139eae0 100644 --- a/crates/next-core/src/next_client/transition.rs +++ b/crates/next-core/src/next_client/transition.rs @@ -95,6 +95,7 @@ impl Transition for NextClientTransition { asset: asset.into(), chunking_context: self.client_chunking_context, base_path: self.server_root.join("_next"), + server_root: self.server_root, runtime_entries: Some(runtime_entries), }; diff --git a/crates/next-core/src/next_client_chunks/with_chunks.rs b/crates/next-core/src/next_client_chunks/with_chunks.rs index 3f2d26b2a8f00..9fb7d9f049b2b 100644 --- a/crates/next-core/src/next_client_chunks/with_chunks.rs +++ b/crates/next-core/src/next_client_chunks/with_chunks.rs @@ -1,4 +1,5 @@ -use anyhow::Result; +use anyhow::{bail, Result}; +use indoc::formatdoc; use serde_json::Value; use turbo_tasks::{primitives::StringVc, TryJoinIterExt, ValueToString, ValueToStringVc}; use turbo_tasks_fs::FileSystemPathVc; @@ -13,14 +14,15 @@ use turbopack::ecmascript::{ use turbopack_core::{ asset::{Asset, AssetContentVc, AssetVc}, chunk::{ - Chunk, ChunkGroupVc, ChunkItem, ChunkItemVc, ChunkVc, ChunkableAsset, - ChunkableAssetReference, ChunkableAssetReferenceVc, ChunkableAssetVc, ChunkingContextVc, - ChunkingType, ChunkingTypeOptionVc, + Chunk, ChunkGroupVc, ChunkItem, ChunkItemVc, ChunkListReferenceVc, ChunkVc, ChunkableAsset, + ChunkableAssetReference, ChunkableAssetReferenceVc, ChunkableAssetVc, ChunkingContext, + ChunkingContextVc, ChunkingType, ChunkingTypeOptionVc, }, ident::AssetIdentVc, reference::{AssetReference, AssetReferenceVc, AssetReferencesVc}, resolve::{ResolveResult, ResolveResultVc}, }; +use turbopack_ecmascript::utils::stringify_js_pretty; use super::in_chunking_context_asset::InChunkingContextAsset; @@ -93,6 +95,25 @@ struct WithChunksChunkItem { inner: WithChunksAssetVc, } +#[turbo_tasks::value_impl] +impl WithChunksChunkItemVc { + #[turbo_tasks::function] + async fn chunk_list_path(self) -> Result { + let this = self.await?; + Ok(this.inner_context.chunk_list_path(this.inner.ident())) + } + + #[turbo_tasks::function] + async fn chunk_group(self) -> Result { + let this = self.await?; + let inner = this.inner.await?; + Ok(ChunkGroupVc::from_asset( + inner.asset.into(), + this.inner_context, + )) + } +} + #[turbo_tasks::value_impl] impl EcmascriptChunkItem for WithChunksChunkItem { #[turbo_tasks::function] @@ -101,29 +122,40 @@ impl EcmascriptChunkItem for WithChunksChunkItem { } #[turbo_tasks::function] - async fn content(&self) -> Result { - let inner = self.inner.await?; - let group = ChunkGroupVc::from_asset(inner.asset.into(), self.inner_context); + async fn content(self_vc: WithChunksChunkItemVc) -> Result { + let this = self_vc.await?; + let inner = this.inner.await?; + let group = self_vc.chunk_group(); let chunks = group.chunks().await?; let server_root = inner.server_root.await?; let mut client_chunks = Vec::new(); + + let chunk_list_path = self_vc.chunk_list_path().await?; + let chunk_list_path = if let Some(path) = server_root.get_path_to(&chunk_list_path) { + path + } else { + bail!("could not get path to chunk list"); + }; + for chunk_path in chunks.iter().map(|c| c.path()).try_join().await? { if let Some(path) = server_root.get_path_to(&chunk_path) { client_chunks.push(Value::String(path.to_string())); } } - let module_id = stringify_js(&*inner.asset.as_chunk_item(self.inner_context).id().await?); + let module_id = stringify_js(&*inner.asset.as_chunk_item(this.inner_context).id().await?); Ok(EcmascriptChunkItemContent { - inner_code: format!( - "__turbopack_esm__({{ - default: () => {}, - chunks: () => chunks -}}); -const chunks = {}; -", + inner_code: formatdoc! { + r#" + __turbopack_esm__({{ + default: () => {}, + chunks: () => {}, + chunkListPath: () => {}, + }}); + "#, module_id, - Value::Array(client_chunks) - ) + stringify_js_pretty(&client_chunks), + stringify_js(&chunk_list_path), + } .into(), ..Default::default() } @@ -139,18 +171,27 @@ impl ChunkItem for WithChunksChunkItem { } #[turbo_tasks::function] - async fn references(&self) -> Result { - let inner = self.inner.await?; - Ok(AssetReferencesVc::cell(vec![WithChunksAssetReference { - asset: InChunkingContextAsset { - asset: inner.asset, - chunking_context: self.inner_context, + async fn references(self_vc: WithChunksChunkItemVc) -> Result { + let this = self_vc.await?; + let inner = this.inner.await?; + Ok(AssetReferencesVc::cell(vec![ + WithChunksAssetReference { + asset: InChunkingContextAsset { + asset: inner.asset, + chunking_context: this.inner_context, + } + .cell() + .into(), } .cell() .into(), - } - .cell() - .into()])) + ChunkListReferenceVc::new( + inner.server_root, + self_vc.chunk_group(), + self_vc.chunk_list_path(), + ) + .into(), + ])) } } diff --git a/crates/next-core/src/next_client_component/with_client_chunks.rs b/crates/next-core/src/next_client_component/with_client_chunks.rs index 82f90787d544b..037059c9d609a 100644 --- a/crates/next-core/src/next_client_component/with_client_chunks.rs +++ b/crates/next-core/src/next_client_component/with_client_chunks.rs @@ -21,6 +21,7 @@ use turbopack_core::{ reference::{AssetReference, AssetReferenceVc, AssetReferencesVc}, resolve::{ResolveResult, ResolveResultVc}, }; +use turbopack_ecmascript::utils::stringify_js_pretty; use crate::next_client_chunks::in_chunking_context_asset::InChunkingContextAsset; @@ -129,15 +130,17 @@ impl EcmascriptChunkItem for WithClientChunksChunkItem { let module_id = stringify_js(&*inner.asset.as_chunk_item(self.context).id().await?); Ok(EcmascriptChunkItemContent { inner_code: formatdoc!( + // We store the chunks in a binding, otherwise a new array would be created every + // time the export binding is read. r#" __turbopack_esm__({{ default: () => __turbopack_import__({}), - chunks: () => chunks + chunks: () => chunks, }}); const chunks = {}; "#, module_id, - stringify_js(&client_chunks) + stringify_js_pretty(&client_chunks), ) .into(), ..Default::default() @@ -154,13 +157,14 @@ impl ChunkItem for WithClientChunksChunkItem { } #[turbo_tasks::function] - async fn references(&self) -> Result { - let inner = self.inner.await?; + async fn references(self_vc: WithClientChunksChunkItemVc) -> Result { + let this = self_vc.await?; + let inner = this.inner.await?; Ok(AssetReferencesVc::cell(vec![ WithClientChunksAssetReference { asset: InChunkingContextAsset { asset: inner.asset, - chunking_context: self.context, + chunking_context: this.context, } .cell() .into(), diff --git a/crates/next-core/src/next_edge/transition.rs b/crates/next-core/src/next_edge/transition.rs index d816dc4611862..f42fe982245bb 100644 --- a/crates/next-core/src/next_edge/transition.rs +++ b/crates/next-core/src/next_edge/transition.rs @@ -112,6 +112,7 @@ impl Transition for NextEdgeTransition { asset: new_asset.into(), chunking_context: self.edge_chunking_context, base_path: self.output_path, + server_root: self.output_path, runtime_entries: None, }; diff --git a/crates/next-core/src/page_loader.rs b/crates/next-core/src/page_loader.rs index 8c766c71f01e2..9e61a8987d13a 100644 --- a/crates/next-core/src/page_loader.rs +++ b/crates/next-core/src/page_loader.rs @@ -2,7 +2,7 @@ use std::io::Write; use anyhow::{bail, Result}; use indexmap::indexmap; -use turbo_tasks::{primitives::StringVc, Value}; +use turbo_tasks::{primitives::StringVc, TryJoinIterExt, Value}; use turbo_tasks_fs::{rope::RopeBuilder, File, FileContent, FileSystemPathVc}; use turbopack_core::{ asset::{Asset, AssetContentVc, AssetVc}, @@ -116,17 +116,28 @@ impl Asset for PageLoaderAsset { let this = &*self_vc.await?; let chunks = self_vc.get_page_chunks().await?; - - let mut data = Vec::with_capacity(chunks.len()); - for chunk in chunks.iter() { - let path = chunk.path().await?; - data.push(serde_json::Value::String(path.path.clone())); - } + let server_root = this.server_root.await?; + + let chunk_paths: Vec<_> = chunks + .iter() + .map(|chunk| { + let server_root = server_root.clone(); + async move { + Ok(server_root + .get_path_to(&*chunk.path().await?) + .map(|path| path.to_string())) + } + }) + .try_join() + .await? + .into_iter() + .flatten() + .collect(); let content = format!( "__turbopack_load_page_chunks__({}, {})\n", stringify_js(&this.pathname.await?), - serde_json::Value::Array(data) + stringify_js(&chunk_paths) ); Ok(AssetContentVc::from(File::from(content))) diff --git a/crates/next-core/src/page_source.rs b/crates/next-core/src/page_source.rs index 4a28dedf1a5da..b8a069b30e3db 100644 --- a/crates/next-core/src/page_source.rs +++ b/crates/next-core/src/page_source.rs @@ -346,7 +346,7 @@ async fn create_page_source_for_file( Value::new(ClientContextType::Pages { pages_dir }), ); - let pathname = pathname_for_path(server_root, server_path, true); + let pathname = pathname_for_path(server_root, server_path, true, false); let route_matcher = NextParamsMatcherVc::new(pathname); Ok(if is_api_path { @@ -370,8 +370,9 @@ async fn create_page_source_for_file( runtime_entries, ) } else { + let data_pathname = pathname_for_path(server_root, server_path, true, true); let data_route_matcher = - NextPrefixSuffixParamsMatcherVc::new(pathname, "_next/data/development/", ".json"); + NextPrefixSuffixParamsMatcherVc::new(data_pathname, "_next/data/development/", ".json"); let ssr_entry = SsrEntry { context: server_context, diff --git a/crates/next-core/src/util.rs b/crates/next-core/src/util.rs index 0b56f35914bb1..15f6a30f46898 100644 --- a/crates/next-core/src/util.rs +++ b/crates/next-core/src/util.rs @@ -28,6 +28,7 @@ pub async fn pathname_for_path( server_root: FileSystemPathVc, server_path: FileSystemPathVc, has_extension: bool, + data: bool, ) -> Result { let server_path_value = &*server_path.await?; let path = if let Some(path) = server_root.await?.get_path_to(server_path_value) { @@ -46,10 +47,14 @@ pub async fn pathname_for_path( } else { path }; - let path = if path == "index" { - "" + let path = if data { + path } else { - path.strip_suffix("/index").unwrap_or(path) + if path == "index" { + "" + } else { + path.strip_suffix("/index").unwrap_or(path) + } }; Ok(StringVc::cell(path.to_string())) diff --git a/crates/next-core/src/web_entry_source.rs b/crates/next-core/src/web_entry_source.rs index 976fd79a73002..51fc056303446 100644 --- a/crates/next-core/src/web_entry_source.rs +++ b/crates/next-core/src/web_entry_source.rs @@ -66,18 +66,23 @@ pub async fn create_web_entry_source( }) .try_join() .await?; - let chunks: Vec<_> = entries + + let chunk_groups: Vec<_> = entries .into_iter() .flatten() .enumerate() .map(|(i, module)| async move { if let Some(ecmascript) = EcmascriptModuleAssetVc::resolve_from(module).await? { - Ok(ecmascript - .as_evaluated_chunk(chunking_context, (i == 0).then_some(runtime_entries))) + let chunk = ecmascript + .as_evaluated_chunk(chunking_context, (i == 0).then_some(runtime_entries)); + let chunk_group = ChunkGroupVc::from_chunk(chunk); + Ok(chunk_group) } else if let Some(chunkable) = ChunkableAssetVc::resolve_from(module).await? { // TODO this is missing runtime code, so it's probably broken and we should also // add an ecmascript chunk with the runtime code - Ok(chunkable.as_chunk(chunking_context)) + Ok(ChunkGroupVc::from_chunk( + chunkable.as_chunk(chunking_context), + )) } else { // TODO convert into a serve-able asset Err(anyhow!( @@ -89,11 +94,7 @@ pub async fn create_web_entry_source( .try_join() .await?; - let entry_asset = DevHtmlAssetVc::new( - server_root.join("index.html"), - chunks.into_iter().map(ChunkGroupVc::from_chunk).collect(), - ) - .into(); + let entry_asset = DevHtmlAssetVc::new(server_root.join("index.html"), chunk_groups).into(); let graph = if eager_compile { AssetGraphContentSourceVc::new_eager(server_root, entry_asset) diff --git a/crates/turbo-tasks/src/trace.rs b/crates/turbo-tasks/src/trace.rs index 518031e7604fc..1d47432375f3b 100644 --- a/crates/turbo-tasks/src/trace.rs +++ b/crates/turbo-tasks/src/trace.rs @@ -115,7 +115,7 @@ impl TraceRawVcs for Vec { } } -impl TraceRawVcs for HashSet { +impl TraceRawVcs for HashSet { fn trace_raw_vcs(&self, context: &mut TraceRawVcsContext) { for item in self.iter() { TraceRawVcs::trace_raw_vcs(item, context); @@ -123,7 +123,7 @@ impl TraceRawVcs for HashSet { } } -impl TraceRawVcs for AutoSet { +impl TraceRawVcs for AutoSet { fn trace_raw_vcs(&self, context: &mut TraceRawVcsContext) { for item in self.iter() { TraceRawVcs::trace_raw_vcs(item, context); @@ -139,7 +139,7 @@ impl TraceRawVcs for BTreeSet { } } -impl TraceRawVcs for IndexSet { +impl TraceRawVcs for IndexSet { fn trace_raw_vcs(&self, context: &mut TraceRawVcsContext) { for item in self.iter() { TraceRawVcs::trace_raw_vcs(item, context); @@ -147,7 +147,7 @@ impl TraceRawVcs for IndexSet { } } -impl TraceRawVcs for HashMap { +impl TraceRawVcs for HashMap { fn trace_raw_vcs(&self, context: &mut TraceRawVcsContext) { for (key, value) in self.iter() { TraceRawVcs::trace_raw_vcs(key, context); @@ -156,7 +156,7 @@ impl TraceRawVcs for HashMap { } } -impl TraceRawVcs for AutoMap { +impl TraceRawVcs for AutoMap { fn trace_raw_vcs(&self, context: &mut TraceRawVcsContext) { for (key, value) in self.iter() { TraceRawVcs::trace_raw_vcs(key, context); @@ -174,7 +174,7 @@ impl TraceRawVcs for BTreeMap { } } -impl TraceRawVcs for IndexMap { +impl TraceRawVcs for IndexMap { fn trace_raw_vcs(&self, context: &mut TraceRawVcsContext) { for (key, value) in self.iter() { TraceRawVcs::trace_raw_vcs(key, context); diff --git a/crates/turbopack-core/src/chunk/dev.rs b/crates/turbopack-core/src/chunk/dev.rs index ec33aaa7b64b9..0e336823e1297 100644 --- a/crates/turbopack-core/src/chunk/dev.rs +++ b/crates/turbopack-core/src/chunk/dev.rs @@ -222,6 +222,11 @@ impl ChunkingContext for DevChunkingContext { Ok(root_path.join(&name)) } + #[turbo_tasks::function] + fn chunk_list_path(self_vc: DevChunkingContextVc, ident: AssetIdentVc) -> FileSystemPathVc { + self_vc.chunk_path(ident.with_modifier(chunk_list_modifier()), ".json") + } + #[turbo_tasks::function] async fn can_be_in_same_chunk(&self, asset_a: AssetVc, asset_b: AssetVc) -> Result { let parent_dir = asset_a.ident().path().parent().await?; @@ -259,3 +264,8 @@ impl ChunkingContext for DevChunkingContext { Ok(DevChunkingContextVc::new(Value::new(context)).into()) } } + +#[turbo_tasks::function] +fn chunk_list_modifier() -> StringVc { + StringVc::cell("chunk list".to_string()) +} diff --git a/crates/turbopack-core/src/chunk/list/asset.rs b/crates/turbopack-core/src/chunk/list/asset.rs new file mode 100644 index 0000000000000..439256dad380f --- /dev/null +++ b/crates/turbopack-core/src/chunk/list/asset.rs @@ -0,0 +1,88 @@ +use anyhow::Result; +use turbo_tasks_fs::FileSystemPathVc; + +use super::content::ChunkListContentVc; +use crate::{ + asset::{Asset, AssetContentVc, AssetVc}, + chunk::{ChunkGroupVc, ChunkReferenceVc, ChunksVc}, + ident::AssetIdentVc, + reference::AssetReferencesVc, + version::{VersionedContent, VersionedContentVc}, +}; + +/// An asset that represents a list of chunks that exist together in a chunk +/// group, and should be *updated* together. +/// +/// A chunk list has no actual content: all it does is merge updates from its +/// chunks into a single update when possible. This is useful for keeping track +/// of changes that affect more than one chunk, or affect the chunk group, e.g.: +/// * moving a module from one chunk to another; +/// * changing a chunk's path. +#[turbo_tasks::value(shared)] +pub(super) struct ChunkListAsset { + server_root: FileSystemPathVc, + chunk_group: ChunkGroupVc, + path: FileSystemPathVc, +} + +#[turbo_tasks::value_impl] +impl ChunkListAssetVc { + /// Creates a new [`ChunkListAsset`]. + #[turbo_tasks::function] + pub fn new( + server_root: FileSystemPathVc, + chunk_group: ChunkGroupVc, + path: FileSystemPathVc, + ) -> Self { + ChunkListAsset { + server_root, + chunk_group, + path, + } + .cell() + } + + #[turbo_tasks::function] + async fn get_chunks(self) -> Result { + Ok(self.await?.chunk_group.chunks()) + } + + #[turbo_tasks::function] + async fn content(self) -> Result { + let this = &*self.await?; + Ok(ChunkListContentVc::new( + this.server_root, + this.chunk_group.chunks(), + )) + } +} + +#[turbo_tasks::value_impl] +impl Asset for ChunkListAsset { + #[turbo_tasks::function] + fn ident(&self) -> AssetIdentVc { + AssetIdentVc::from_path(self.path) + } + + #[turbo_tasks::function] + async fn references(&self) -> Result { + let chunks = self.chunk_group.chunks().await?; + + let mut references = Vec::with_capacity(chunks.len()); + for chunk in chunks.iter() { + references.push(ChunkReferenceVc::new(*chunk).into()); + } + + Ok(AssetReferencesVc::cell(references)) + } + + #[turbo_tasks::function] + fn content(self_vc: ChunkListAssetVc) -> AssetContentVc { + self_vc.content().content() + } + + #[turbo_tasks::function] + fn versioned_content(self_vc: ChunkListAssetVc) -> VersionedContentVc { + self_vc.content().into() + } +} diff --git a/crates/turbopack-core/src/chunk/list/content.rs b/crates/turbopack-core/src/chunk/list/content.rs new file mode 100644 index 0000000000000..7e6e05b7adfc9 --- /dev/null +++ b/crates/turbopack-core/src/chunk/list/content.rs @@ -0,0 +1,119 @@ +use anyhow::Result; +use indexmap::IndexMap; +use turbo_tasks::{IntoTraitRef, TryJoinIterExt}; +use turbo_tasks_fs::{FileContent, FileSystemPathReadRef, FileSystemPathVc}; + +use super::{ + update::update_chunk_list, + version::{ChunkListVersion, ChunkListVersionVc}, +}; +use crate::{ + asset::{Asset, AssetContent, AssetContentVc}, + chunk::ChunksVc, + version::{ + MergeableVersionedContent, MergeableVersionedContentVc, UpdateVc, VersionVc, + VersionedContent, VersionedContentMerger, VersionedContentVc, VersionedContentsVc, + }, +}; + +/// Contents of a [`super::asset::ChunkListAsset`]. +#[turbo_tasks::value] +pub(super) struct ChunkListContent { + pub server_root: FileSystemPathReadRef, + pub chunks_contents: IndexMap, +} + +#[turbo_tasks::value_impl] +impl ChunkListContentVc { + /// Creates a new [`ChunkListContent`]. + #[turbo_tasks::function] + pub async fn new(server_root: FileSystemPathVc, chunks: ChunksVc) -> Result { + let server_root = server_root.await?; + Ok(ChunkListContent { + server_root: server_root.clone(), + chunks_contents: chunks + .await? + .iter() + .map(|chunk| { + let server_root = server_root.clone(); + async move { + Ok(( + server_root + .get_path_to(&*chunk.ident().path().await?) + .map(|path| path.to_string()), + chunk.versioned_content(), + )) + } + }) + .try_join() + .await? + .into_iter() + .filter_map(|(path, content)| path.map(|path| (path, content))) + .collect(), + } + .cell()) + } + + /// Computes the version of this content. + #[turbo_tasks::function] + pub async fn version(self) -> Result { + let this = self.await?; + + let mut by_merger = IndexMap::<_, Vec<_>>::new(); + let mut by_path = IndexMap::<_, _>::new(); + + for (chunk_path, chunk_content) in &this.chunks_contents { + if let Some(mergeable) = + MergeableVersionedContentVc::resolve_from(chunk_content).await? + { + let merger = mergeable.get_merger().resolve().await?; + by_merger.entry(merger).or_default().push(*chunk_content); + } else { + by_path.insert( + chunk_path.clone(), + chunk_content.version().into_trait_ref().await?, + ); + } + } + + let by_merger = by_merger + .into_iter() + .map(|(merger, contents)| { + let merger = merger; + async move { + Ok(( + merger, + merger + .merge(VersionedContentsVc::cell(contents)) + .version() + .into_trait_ref() + .await?, + )) + } + }) + .try_join() + .await? + .into_iter() + .collect(); + + Ok(ChunkListVersion { by_path, by_merger }.cell()) + } +} + +#[turbo_tasks::value_impl] +impl VersionedContent for ChunkListContent { + #[turbo_tasks::function] + fn content(&self) -> AssetContentVc { + AssetContentVc::cell(AssetContent::File(FileContent::NotFound.into())) + } + + #[turbo_tasks::function] + fn version(self_vc: ChunkListContentVc) -> VersionVc { + self_vc.version().into() + } + + #[turbo_tasks::function] + fn update(self_vc: ChunkListContentVc, from_version: VersionVc) -> UpdateVc { + update_chunk_list(self_vc, from_version) + } +} diff --git a/crates/turbopack-core/src/chunk/list/mod.rs b/crates/turbopack-core/src/chunk/list/mod.rs new file mode 100644 index 0000000000000..829280a02890e --- /dev/null +++ b/crates/turbopack-core/src/chunk/list/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod asset; +pub(crate) mod content; +pub(crate) mod reference; +pub(crate) mod update; +pub(crate) mod version; diff --git a/crates/turbopack-core/src/chunk/list/reference.rs b/crates/turbopack-core/src/chunk/list/reference.rs new file mode 100644 index 0000000000000..cf9a811ac0bfc --- /dev/null +++ b/crates/turbopack-core/src/chunk/list/reference.rs @@ -0,0 +1,74 @@ +use anyhow::Result; +use turbo_tasks::{primitives::StringVc, ValueToString, ValueToStringVc}; +use turbo_tasks_fs::FileSystemPathVc; + +use super::asset::ChunkListAssetVc; +use crate::{ + chunk::{ + ChunkGroupVc, ChunkableAssetReference, ChunkableAssetReferenceVc, ChunkingContextVc, + ChunkingType, ChunkingTypeOptionVc, + }, + reference::{AssetReference, AssetReferenceVc}, + resolve::{ResolveResult, ResolveResultVc}, +}; + +/// A reference to a [`ChunkListAsset`]. +/// +/// This is the only way to create a [`ChunkListAsset`]. The asset itself will +/// live under the provided path. +/// +/// [`ChunkListAsset`]: super::asset::ChunkListAsset +#[turbo_tasks::value] +pub struct ChunkListReference { + server_root: FileSystemPathVc, + chunk_group: ChunkGroupVc, + path: FileSystemPathVc, +} + +#[turbo_tasks::value_impl] +impl ChunkListReferenceVc { + /// Creates a new [`ChunkListReference`]. + #[turbo_tasks::function] + pub fn new( + server_root: FileSystemPathVc, + chunk_group: ChunkGroupVc, + path: FileSystemPathVc, + ) -> Self { + ChunkListReference { + server_root, + chunk_group, + path, + } + .cell() + } +} + +#[turbo_tasks::value_impl] +impl ValueToString for ChunkListReference { + #[turbo_tasks::function] + async fn to_string(&self) -> Result { + Ok(StringVc::cell(format!( + "referenced chunk list {}", + self.path.to_string().await? + ))) + } +} + +#[turbo_tasks::value_impl] +impl AssetReference for ChunkListReference { + #[turbo_tasks::function] + fn resolve_reference(&self) -> ResolveResultVc { + ResolveResult::asset( + ChunkListAssetVc::new(self.server_root, self.chunk_group, self.path).into(), + ) + .cell() + } +} + +#[turbo_tasks::value_impl] +impl ChunkableAssetReference for ChunkListReference { + #[turbo_tasks::function] + fn chunking_type(&self, _context: ChunkingContextVc) -> ChunkingTypeOptionVc { + ChunkingTypeOptionVc::cell(Some(ChunkingType::Separate)) + } +} diff --git a/crates/turbopack-core/src/chunk/list/update.rs b/crates/turbopack-core/src/chunk/list/update.rs new file mode 100644 index 0000000000000..4bbbefc0cc7e2 --- /dev/null +++ b/crates/turbopack-core/src/chunk/list/update.rs @@ -0,0 +1,166 @@ +use anyhow::Result; +use indexmap::IndexMap; +use serde::Serialize; +use turbo_tasks::{ + primitives::{JsonValueReadRef, JsonValueVc}, + TraitRef, +}; + +use super::{content::ChunkListContentVc, version::ChunkListVersionVc}; +use crate::version::{ + MergeableVersionedContent, MergeableVersionedContentVc, PartialUpdate, TotalUpdate, Update, + UpdateVc, VersionVc, VersionedContent, VersionedContentMerger, VersionedContentsVc, +}; + +/// Update of a chunk list from one version to another. +#[derive(Serialize)] +#[serde(tag = "type")] +#[serde(rename_all = "camelCase")] +struct ChunkListUpdate<'a> { + /// A map from chunk path to a corresponding update of that chunk. + #[serde(skip_serializing_if = "IndexMap::is_empty")] + chunks: IndexMap<&'a str, ChunkUpdate>, + /// List of merged updates since the last version. + #[serde(skip_serializing_if = "Vec::is_empty")] + merged: Vec, +} + +/// Update of a chunk from one version to another. +#[derive(Serialize)] +#[serde(tag = "type")] +#[serde(rename_all = "camelCase")] +enum ChunkUpdate { + /// The chunk was updated and must be reloaded. + Total, + /// The chunk was updated and can be merged with the previous version. + Partial { instruction: JsonValueReadRef }, + /// The chunk was added. + Added, + /// The chunk was deleted. + Deleted, +} + +impl<'a> ChunkListUpdate<'a> { + /// Returns `true` if this update is empty. + fn is_empty(&self) -> bool { + let ChunkListUpdate { chunks, merged } = self; + chunks.is_empty() && merged.is_empty() + } +} + +/// Computes the update of a chunk list from one version to another. +#[turbo_tasks::function] +pub(super) async fn update_chunk_list( + content: ChunkListContentVc, + from_version: VersionVc, +) -> Result { + let to_version = content.version(); + let from_version = if let Some(from) = ChunkListVersionVc::resolve_from(from_version).await? { + from + } else { + // It's likely `from_version` is `NotFoundVersion`. + return Ok(Update::Total(TotalUpdate { + to: to_version.into(), + }) + .cell()); + }; + + let to = to_version.await?; + let from = from_version.await?; + + // When to and from point to the same value we can skip comparing them. + // This will happen since `TraitRef::cell` will not clone the value, + // but only make the cell point to the same immutable value (Arc). + if from.ptr_eq(&to) { + return Ok(Update::None.cell()); + } + + let content = content.await?; + + // There are two kind of updates nested within a chunk list update: + // * merged updates; and + // * single chunk updates. + // In order to compute merged updates, we first need to group mergeable chunks + // by common mergers. Then, we compute the update of each group separately. + // Single chunk updates are computed separately and only require a stable chunk + // path to identify the chunk across versions. + let mut by_merger = IndexMap::<_, Vec<_>>::new(); + let mut by_path = IndexMap::<_, _>::new(); + + for (chunk_path, chunk_content) in &content.chunks_contents { + if let Some(mergeable) = MergeableVersionedContentVc::resolve_from(chunk_content).await? { + let merger = mergeable.get_merger().resolve().await?; + by_merger.entry(merger).or_default().push(*chunk_content); + } else { + by_path.insert(chunk_path, chunk_content); + } + } + + let mut chunks = IndexMap::<_, _>::new(); + + for (chunk_path, from_chunk_version) in &from.by_path { + if let Some(chunk_content) = by_path.remove(chunk_path) { + let chunk_update = chunk_content + .update(TraitRef::cell(from_chunk_version.clone())) + .await?; + + match &*chunk_update { + Update::Total(_) => { + chunks.insert(chunk_path.as_ref(), ChunkUpdate::Total); + } + Update::Partial(partial) => { + chunks.insert( + chunk_path.as_ref(), + ChunkUpdate::Partial { + instruction: partial.instruction.await?, + }, + ); + } + Update::None => {} + } + } else { + chunks.insert(chunk_path.as_ref(), ChunkUpdate::Deleted); + } + } + + for chunk_path in by_path.keys() { + chunks.insert(chunk_path.as_ref(), ChunkUpdate::Added); + } + + let mut merged = vec![]; + + for (merger, chunks_contents) in by_merger { + if let Some(from_version) = from.by_merger.get(&merger) { + let content = merger.merge(VersionedContentsVc::cell(chunks_contents)); + + let chunk_update = content.update(TraitRef::cell(from_version.clone())).await?; + + match &*chunk_update { + // Getting a total or not found update from a merger is unexpected. If it happens, + // we have no better option than to short-circuit the update. + Update::Total(_) => { + return Ok(Update::Total(TotalUpdate { + to: to_version.into(), + }) + .cell()); + } + Update::Partial(partial) => { + merged.push(partial.instruction.await?); + } + Update::None => {} + } + } + } + let update = ChunkListUpdate { chunks, merged }; + + let update = if update.is_empty() { + Update::None + } else { + Update::Partial(PartialUpdate { + to: to_version.into(), + instruction: JsonValueVc::cell(serde_json::to_value(&update)?), + }) + }; + + Ok(update.into()) +} diff --git a/crates/turbopack-core/src/chunk/list/version.rs b/crates/turbopack-core/src/chunk/list/version.rs new file mode 100644 index 0000000000000..a73dbec807546 --- /dev/null +++ b/crates/turbopack-core/src/chunk/list/version.rs @@ -0,0 +1,64 @@ +use anyhow::Result; +use indexmap::IndexMap; +use turbo_tasks::{primitives::StringVc, TraitRef, TryJoinIterExt}; +use turbo_tasks_hash::{encode_hex, Xxh3Hash64Hasher}; + +use crate::version::{Version, VersionVc, VersionedContentMergerVc}; + +/// The version of a [`ChunkListContent`]. +/// +/// [`ChunkListContent`]: super::content::ChunkListContent +#[turbo_tasks::value(shared)] +pub(super) struct ChunkListVersion { + /// A map from chunk path to its version. + #[turbo_tasks(trace_ignore)] + pub by_path: IndexMap>, + /// A map from chunk merger to the version of the merged contents of chunks. + #[turbo_tasks(trace_ignore)] + pub by_merger: IndexMap>, +} + +#[turbo_tasks::value_impl] +impl Version for ChunkListVersion { + #[turbo_tasks::function] + async fn id(&self) -> Result { + let by_path = { + let mut by_path = self + .by_path + .iter() + .map(|(path, version)| async move { + let id = TraitRef::cell(version.clone()).id().await?.clone_value(); + Ok((path, id)) + }) + .try_join() + .await?; + by_path.sort(); + by_path + }; + let by_merger = { + let mut by_merger = self + .by_merger + .iter() + .map(|(_merger, version)| async move { + Ok(TraitRef::cell(version.clone()).id().await?.clone_value()) + }) + .try_join() + .await?; + by_merger.sort(); + by_merger + }; + let mut hasher = Xxh3Hash64Hasher::new(); + hasher.write_value(by_path.len()); + for (path, id) in by_path { + hasher.write_value(path); + hasher.write_value(id); + } + hasher.write_value(by_merger.len()); + for id in by_merger { + hasher.write_value(id); + } + let hash = hasher.finish(); + let hex_hash = encode_hex(hash); + Ok(StringVc::cell(hex_hash)) + } +} diff --git a/crates/turbopack-core/src/chunk/mod.rs b/crates/turbopack-core/src/chunk/mod.rs index e17267cbea021..e5594310357b0 100644 --- a/crates/turbopack-core/src/chunk/mod.rs +++ b/crates/turbopack-core/src/chunk/mod.rs @@ -1,5 +1,6 @@ pub mod chunk_in_group; pub mod dev; +pub(crate) mod list; pub mod optimize; use std::{ @@ -24,6 +25,7 @@ use turbo_tasks::{ use turbo_tasks_fs::FileSystemPathVc; use turbo_tasks_hash::DeterministicHash; +pub use self::list::reference::{ChunkListReference, ChunkListReferenceVc}; use self::{chunk_in_group::ChunkInGroupVc, optimize::optimize}; use crate::{ asset::{Asset, AssetVc, AssetsVc}, @@ -35,7 +37,7 @@ use crate::{ /// A module id, which can be a number or string #[turbo_tasks::value(shared)] -#[derive(Debug, Clone, Hash, DeterministicHash)] +#[derive(Debug, Clone, Hash, Ord, PartialOrd, DeterministicHash)] #[serde(untagged)] pub enum ModuleId { Number(u32), @@ -83,6 +85,10 @@ pub trait ChunkingContext { fn chunk_path(&self, ident: AssetIdentVc, extension: &str) -> FileSystemPathVc; + /// Returns the path to the chunk list file for the given entry module + /// ident. + fn chunk_list_path(&self, ident: AssetIdentVc) -> FileSystemPathVc; + fn can_be_in_same_chunk(&self, asset_a: AssetVc, asset_b: AssetVc) -> BoolVc; fn asset_path(&self, content_hash: &str, extension: &str) -> FileSystemPathVc; diff --git a/crates/turbopack-core/src/version.rs b/crates/turbopack-core/src/version.rs index b5dc6bc6515ea..e421540425ed2 100644 --- a/crates/turbopack-core/src/version.rs +++ b/crates/turbopack-core/src/version.rs @@ -127,6 +127,26 @@ pub trait Version { fn id(&self) -> StringVc; } +/// This trait allows multiple `VersionedContent` to declare which +/// [`VersionedContentMerger`] implementation should be used for merging. +/// +/// [`MergeableVersionedContent`] which return the same merger will be merged +/// together. +#[turbo_tasks::value_trait] +pub trait MergeableVersionedContent: VersionedContent { + fn get_merger(&self) -> VersionedContentMergerVc; +} + +/// A [`VersionedContentMerger`] merges multiple [`VersionedContent`] into a +/// single one. +#[turbo_tasks::value_trait] +pub trait VersionedContentMerger { + fn merge(&self, contents: VersionedContentsVc) -> VersionedContentVc; +} + +#[turbo_tasks::value(transparent)] +pub struct VersionedContents(Vec); + #[turbo_tasks::value] pub struct NotFoundVersion; diff --git a/crates/turbopack-dev-server/src/html.rs b/crates/turbopack-dev-server/src/html.rs index 38d4b87be5b73..4608b0b694637 100644 --- a/crates/turbopack-dev-server/src/html.rs +++ b/crates/turbopack-dev-server/src/html.rs @@ -1,6 +1,6 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, Result}; use mime_guess::mime::TEXT_HTML_UTF_8; -use turbo_tasks::{debug::ValueDebug, primitives::StringVc}; +use turbo_tasks::primitives::StringVc; use turbo_tasks_fs::{File, FileSystemPathVc}; use turbo_tasks_hash::{encode_hex, Xxh3Hash64Hasher}; use turbopack_core::{ @@ -8,7 +8,7 @@ use turbopack_core::{ chunk::{Chunk, ChunkGroupVc, ChunkReferenceVc}, ident::AssetIdentVc, reference::AssetReferencesVc, - version::{Update, UpdateVc, Version, VersionVc, VersionedContent, VersionedContentVc}, + version::{Version, VersionVc, VersionedContent, VersionedContentVc}, }; /// The HTML entry point of the dev server. @@ -183,28 +183,6 @@ impl VersionedContent for DevHtmlAssetContent { fn version(self_vc: DevHtmlAssetContentVc) -> VersionVc { self_vc.version().into() } - - #[turbo_tasks::function] - async fn update(self_vc: DevHtmlAssetContentVc, from_version: VersionVc) -> Result { - let from_version = DevHtmlAssetVersionVc::resolve_from(from_version) - .await? - .context("version must be an `DevHtmlAssetVersionVc`")?; - let to_version = self_vc.version(); - - let to = to_version.await?; - let from = from_version.await?; - - if to.content.chunk_paths == from.content.chunk_paths { - return Ok(Update::None.into()); - } - - Err(anyhow!( - "cannot update `DevHtmlAssetContentVc` from version {:?} to version {:?}: the \ - versions contain different chunks, which is not yet supported", - from_version.dbg().await?, - to_version.dbg().await?, - )) - } } #[turbo_tasks::value] diff --git a/crates/turbopack-dev-server/src/source/mod.rs b/crates/turbopack-dev-server/src/source/mod.rs index b08ee62c47d78..e5b5c36eb5576 100644 --- a/crates/turbopack-dev-server/src/source/mod.rs +++ b/crates/turbopack-dev-server/src/source/mod.rs @@ -202,12 +202,6 @@ pub struct NeededData { pub vary: ContentSourceDataVary, } -impl From for ContentSourceContentVc { - fn from(content: VersionedContentVc) -> Self { - ContentSourceContentVc::static_content(content) - } -} - /// Additional info passed to the ContentSource. It was extracted from the http /// request. /// diff --git a/crates/turbopack-dev-server/src/source/resolve.rs b/crates/turbopack-dev-server/src/source/resolve.rs index 3b549ddac471a..5207dd7d8443f 100644 --- a/crates/turbopack-dev-server/src/source/resolve.rs +++ b/crates/turbopack-dev-server/src/source/resolve.rs @@ -85,7 +85,7 @@ pub async fn resolve_source_request( } current_asset_path = new_asset_path; data = ContentSourceData::default(); - } // _ => , + } ContentSourceContent::NotFound => { break Ok(ResolveSourceRequestResult::NotFound.cell()) } diff --git a/crates/turbopack-dev-server/src/update/protocol.rs b/crates/turbopack-dev-server/src/update/protocol.rs index 3168f46a58f68..880fb4d6ee9d3 100644 --- a/crates/turbopack-dev-server/src/update/protocol.rs +++ b/crates/turbopack-dev-server/src/update/protocol.rs @@ -56,6 +56,12 @@ impl<'a> ClientUpdateInstruction<'a> { Self::new(resource, ClientUpdateInstructionType::Restart, issues) } + /// Returns a [`ClientUpdateInstruction`] that indicates that the resource + /// was not found. + pub fn not_found(resource: &'a ResourceIdentifier) -> Self { + Self::new(resource, ClientUpdateInstructionType::NotFound, &[]) + } + pub fn partial( resource: &'a ResourceIdentifier, instruction: &'a Value, @@ -85,6 +91,7 @@ impl<'a> ClientUpdateInstruction<'a> { #[serde(tag = "type", rename_all = "camelCase")] pub enum ClientUpdateInstructionType<'a> { Restart, + NotFound, Partial { instruction: &'a Value }, Issues, } diff --git a/crates/turbopack-dev-server/src/update/server.rs b/crates/turbopack-dev-server/src/update/server.rs index d4719ac194199..e5da3430de5c1 100644 --- a/crates/turbopack-dev-server/src/update/server.rs +++ b/crates/turbopack-dev-server/src/update/server.rs @@ -85,7 +85,7 @@ impl UpdateServer

{ } } Some((resource, update)) = streams.next() => { - Self::send_update(&mut client, resource, &update).await?; + Self::send_update(&mut client, &mut streams, resource, &update).await?; } else => break } @@ -96,35 +96,46 @@ impl UpdateServer

{ async fn send_update( client: &mut UpdateClient, + streams: &mut StreamMap, resource: ResourceIdentifier, - update: &UpdateStreamItem, + item: &UpdateStreamItem, ) -> Result<()> { - let issues = update - .issues - .iter() - .map(|p| (&**p).into()) - .collect::>>(); - - match &*update.update { - Update::Partial(partial) => { - let partial_instruction = partial.instruction.await?; + match item { + UpdateStreamItem::NotFound => { + // If the resource was not found, we remove the stream and indicate that to the + // client. + streams.remove(&resource); client - .send(ClientUpdateInstruction::partial( - &resource, - &partial_instruction, - &issues, - )) + .send(ClientUpdateInstruction::not_found(&resource)) .await?; } - Update::Total(_total) => { - client - .send(ClientUpdateInstruction::restart(&resource, &issues)) - .await?; - } - Update::None => { - client - .send(ClientUpdateInstruction::issues(&resource, &issues)) - .await?; + UpdateStreamItem::Found { update, issues } => { + let issues = issues + .iter() + .map(|p| (&**p).into()) + .collect::>>(); + match &**update { + Update::Partial(partial) => { + let partial_instruction = partial.instruction.await?; + client + .send(ClientUpdateInstruction::partial( + &resource, + &partial_instruction, + &issues, + )) + .await?; + } + Update::Total(_total) => { + client + .send(ClientUpdateInstruction::restart(&resource, &issues)) + .await?; + } + Update::None => { + client + .send(ClientUpdateInstruction::issues(&resource, &issues)) + .await?; + } + } } } diff --git a/crates/turbopack-dev-server/src/update/stream.rs b/crates/turbopack-dev-server/src/update/stream.rs index 0bd652fde3c2e..977348bc9a5f4 100644 --- a/crates/turbopack-dev-server/src/update/stream.rs +++ b/crates/turbopack-dev-server/src/update/stream.rs @@ -41,16 +41,25 @@ async fn get_update_stream_item( let content = get_content(); match &*content.await? { - ResolveSourceRequestResult::Static(static_content, _) => { - let resolved_content = static_content.await?.content; + ResolveSourceRequestResult::Static(static_content_vc, _) => { + let static_content = static_content_vc.await?; + + // This can happen when a chunk is removed from the asset graph. + if static_content.status_code == 404 { + return Ok(UpdateStreamItem::NotFound.cell()); + } + + let resolved_content = static_content.content; let from = from.get(); let update = resolved_content.update(from); let mut plain_issues = peek_issues(update).await?; extend_issues(&mut plain_issues, peek_issues(content).await?); - Ok(UpdateStreamItem { - update: update.await?, + let update = update.await?; + + Ok(UpdateStreamItem::Found { + update, issues: plain_issues, } .cell()) @@ -71,7 +80,7 @@ async fn get_update_stream_item( Update::None.cell() }; - Ok(UpdateStreamItem { + Ok(UpdateStreamItem::Found { update: update.await?, issues: plain_issues, } @@ -148,28 +157,42 @@ impl UpdateStream { let mut last_had_issues = false; - let stream = ReceiverStream::new(rx).filter_map(move |update| { - let has_issues = !update.issues.is_empty(); - let issues_changed = has_issues != last_had_issues; - last_had_issues = has_issues; + let stream = ReceiverStream::new(rx).filter_map(move |item| { + let (has_issues, issues_changed) = + if let UpdateStreamItem::Found { issues, .. } = &*item { + let has_issues = !issues.is_empty(); + let issues_changed = has_issues != last_had_issues; + last_had_issues = has_issues; + (has_issues, issues_changed) + } else { + (false, false) + }; async move { - match &*update.update { - Update::Partial(PartialUpdate { to, .. }) - | Update::Total(TotalUpdate { to }) => { - version_state - .set(*to) - .await - .expect("failed to update version"); - - Some(update) + match &*item { + UpdateStreamItem::NotFound => { + // Propagate not found updates so we can drop this update stream. + Some(item) } - // Do not propagate empty updates. - Update::None => { - if has_issues || issues_changed { - Some(update) - } else { - None + UpdateStreamItem::Found { update, .. } => { + match &**update { + Update::Partial(PartialUpdate { to, .. }) + | Update::Total(TotalUpdate { to }) => { + version_state + .set(*to) + .await + .expect("failed to update version"); + + Some(item) + } + // Do not propagate empty updates. + Update::None => { + if has_issues || issues_changed { + Some(item) + } else { + None + } + } } } } @@ -192,7 +215,11 @@ impl Stream for UpdateStream { } #[turbo_tasks::value(serialization = "none")] -pub struct UpdateStreamItem { - pub update: UpdateReadRef, - pub issues: Vec, +#[derive(Debug)] +pub enum UpdateStreamItem { + NotFound, + Found { + update: UpdateReadRef, + issues: Vec, + }, } diff --git a/crates/turbopack-ecmascript/js/src/runtime.dom.js b/crates/turbopack-ecmascript/js/src/runtime.dom.js index 3916e66eef60a..82d4aef9a799f 100644 --- a/crates/turbopack-ecmascript/js/src/runtime.dom.js +++ b/crates/turbopack-ecmascript/js/src/runtime.dom.js @@ -2,7 +2,7 @@ /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -33,5 +33,64 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; diff --git a/crates/turbopack-ecmascript/js/src/runtime.js b/crates/turbopack-ecmascript/js/src/runtime.js index 10d5db91aa5f9..c3a79272dcc73 100644 --- a/crates/turbopack-ecmascript/js/src/runtime.js +++ b/crates/turbopack-ecmascript/js/src/runtime.js @@ -20,8 +20,11 @@ /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -73,6 +76,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -347,6 +373,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -488,7 +515,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -499,18 +526,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -527,7 +556,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -559,35 +588,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -600,10 +638,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -637,24 +680,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -677,22 +723,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + chunksAdded.set(chunkPath, updateAdded); + chunksDeleted.set(chunkPath, updateDeleted); + break; + } + default: + invariant( + mergedChunkUpdate, + (mergedChunkUpdate) => + `Unknown merged chunk update type: ${mergedChunkUpdate.type}` + ); + } + } - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); + // 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 }; } /** @@ -782,17 +984,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -895,10 +1115,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -923,18 +1153,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -944,18 +1238,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); } +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -976,6 +1309,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -983,7 +1317,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-ecmascript/js/types/index.d.ts b/crates/turbopack-ecmascript/js/types/index.d.ts index c47e55a70631b..e2ae613e9e1c8 100644 --- a/crates/turbopack-ecmascript/js/types/index.d.ts +++ b/crates/turbopack-ecmascript/js/types/index.d.ts @@ -73,10 +73,13 @@ interface Runtime { cache: Record; instantiateRuntimeModule: (moduleId: ModuleId) => Module; + registerChunkList: (chunkPath: ChunkPath, chunkPaths: ChunkPath[]) => void; } interface RuntimeBackend { - loadChunk: (chunkPath: ChunkPath, from: ModuleId) => Promise; + loadChunk: (chunkPath: ChunkPath, from?: ModuleId) => Promise; + reloadChunk?: (chunkPath: ChunkPath) => Promise; + unloadChunk?: (chunkPath: ChunkPath) => void; restart: () => void; } diff --git a/crates/turbopack-ecmascript/js/types/protocol.d.ts b/crates/turbopack-ecmascript/js/types/protocol.d.ts index a4271bfd62bd9..b269a2341833f 100644 --- a/crates/turbopack-ecmascript/js/types/protocol.d.ts +++ b/crates/turbopack-ecmascript/js/types/protocol.d.ts @@ -7,9 +7,12 @@ export type ServerMessage = { | { type: "restart"; } + | { + type: "notFound"; + } | { type: "partial"; - instruction: EcmascriptChunkUpdate; + instruction: PartialUpdate; } | { type: "issues"; @@ -21,14 +24,59 @@ type UnknownType = { type: "future-type-marker-do-not-use-or-you-will-be-fired"; }; -export type EcmascriptChunkUpdate = { - type: "EcmascriptChunkUpdate"; - added: Record; - modified: Record; - deleted: ModuleId[]; +export type PartialUpdate = + | ChunkListUpdate + | { + type: never; + }; + +export type ChunkListUpdate = { + type: "ChunkListUpdate"; + chunks?: Record; + merged?: MergedChunkUpdate[]; +}; + +export type ChunkUpdate = + | { + type: "added"; + } + | { type: "deleted" } + | { type: "total" } + // We currently don't have any chunks that can be updated partially that can't + // be merged either. So these updates would go into `MergedChunkUpdate` instead. + | { type: "partial"; instruction: never }; + +export type MergedChunkUpdate = + | EcmascriptMergedUpdate + | { + type: never; + }; + +export type EcmascriptMergedUpdate = { + type: "EcmascriptMergedUpdate"; + entries?: Record; + chunks?: Record; }; -export type HmrUpdateEntry = { +export type EcmascriptMergedChunkUpdate = + | { + type: "added"; + modules?: ModuleId[]; + } + | { + type: "deleted"; + modules?: ModuleId[]; + } + | { + type: "partial"; + added?: ModuleId[]; + deleted?: ModuleId[]; + } + | { + type: never; + }; + +export type EcmascriptModuleEntry = { code: ModuleFactoryString; url: string; map?: string; diff --git a/crates/turbopack-ecmascript/src/chunk/content.rs b/crates/turbopack-ecmascript/src/chunk/content.rs index 3f1177815181b..b094fede660b9 100644 --- a/crates/turbopack-ecmascript/src/chunk/content.rs +++ b/crates/turbopack-ecmascript/src/chunk/content.rs @@ -15,15 +15,18 @@ use turbopack_core::{ environment::{ChunkLoading, EnvironmentVc}, reference::AssetReferenceVc, source_map::{GenerateSourceMap, GenerateSourceMapVc, OptionSourceMapVc, SourceMapVc}, - version::{UpdateVc, VersionVc, VersionedContent, VersionedContentVc}, + version::{ + MergeableVersionedContent, MergeableVersionedContentVc, UpdateVc, VersionVc, + VersionedContent, VersionedContentMergerVc, VersionedContentVc, + }, }; use super::{ evaluate::EcmascriptChunkContentEvaluateVc, item::{EcmascriptChunkItemVc, EcmascriptChunkItems, EcmascriptChunkItemsVc}, + merged::merger::EcmascriptChunkContentMergerVc, placeable::{EcmascriptChunkPlaceableVc, EcmascriptChunkPlaceablesVc}, snapshot::EcmascriptChunkContentEntriesSnapshotReadRef, - update::update_ecmascript_chunk, version::{EcmascriptChunkVersion, EcmascriptChunkVersionVc}, }; use crate::utils::stringify_js; @@ -174,7 +177,7 @@ impl EcmascriptChunkContentVc { #[turbo_tasks::value_impl] impl EcmascriptChunkContentVc { #[turbo_tasks::function] - pub(super) async fn version(self) -> Result { + pub(super) async fn own_version(self) -> Result { let this = self.await?; let chunk_server_path = if let Some(path) = this.output_root.get_path_to(&this.chunk_path) { path @@ -234,12 +237,29 @@ impl EcmascriptChunkContentVc { .map(|id| async move { let id = id.await?; let id = stringify_js(&id); - Ok(format!(r#"instantiateRuntimeModule({id});"#)) as Result<_> + Ok(format!(r#" instantiateRuntimeModule({id});"#)) as Result<_> }) .try_join() .await? .join("\n"); + let chunk_list_register = evaluate + .chunk_list_path + .as_deref() + .map(|path| { + format!( + r#"registerChunkList({}, {});"#, + stringify_js(&path), + stringify_js( + &evaluate + .ecma_chunks_server_paths + .iter() + .chain(&evaluate.other_chunks_server_paths) + .collect::>() + ) + ) + }) + .unwrap_or_else(String::new); // Add a runnable to the chunk that requests the entry module to ensure it gets // executed when the chunk is evaluated. // The condition stops the entry module from being executed while chunks it @@ -249,9 +269,10 @@ impl EcmascriptChunkContentVc { writedoc!( code, r#" - , ({{ loadedChunks, instantiateRuntimeModule }}) => {{ - if(!(true{condition})) return true; - {entries_instantiations} + , ({{ loadedChunks, instantiateRuntimeModule, registerChunkList }}) => {{ + if (!(true{condition})) return true; + {chunk_list_register} + {entries_instantiations} }} "# )?; @@ -322,12 +343,20 @@ impl VersionedContent for EcmascriptChunkContent { #[turbo_tasks::function] fn version(self_vc: EcmascriptChunkContentVc) -> VersionVc { - self_vc.version().into() + self_vc.own_version().into() } #[turbo_tasks::function] - fn update(self_vc: EcmascriptChunkContentVc, from_version: VersionVc) -> UpdateVc { - update_ecmascript_chunk(self_vc, from_version) + fn update(_self_vc: EcmascriptChunkContentVc, _from_version: VersionVc) -> Result { + bail!("EcmascriptChunkContent is not updateable") + } +} + +#[turbo_tasks::value_impl] +impl MergeableVersionedContent for EcmascriptChunkContent { + #[turbo_tasks::function] + fn get_merger(&self) -> VersionedContentMergerVc { + EcmascriptChunkContentMergerVc::new().into() } } diff --git a/crates/turbopack-ecmascript/src/chunk/evaluate.rs b/crates/turbopack-ecmascript/src/chunk/evaluate.rs index cf55359f1204b..2b266fee1f549 100644 --- a/crates/turbopack-ecmascript/src/chunk/evaluate.rs +++ b/crates/turbopack-ecmascript/src/chunk/evaluate.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use turbo_tasks_fs::FileSystemPathVc; use turbopack_core::chunk::{ chunk_in_group::ChunkInGroupVc, Chunk, ChunkGroupVc, ChunkingContext, ChunkingContextVc, ModuleIdVc, @@ -20,6 +21,9 @@ pub struct EcmascriptChunkEvaluate { /// All chunks of this chunk group need to be ready for execution to start. /// When None, it will use a chunk group created from the current chunk. pub chunk_group: Option, + /// The path to the chunk list asset. This will be used to register the + /// chunk list when this chunk is evaluated. + pub chunk_list_path: Option, } #[turbo_tasks::value_impl] @@ -33,6 +37,7 @@ impl EcmascriptChunkEvaluateVc { let &EcmascriptChunkEvaluate { evaluate_entries, chunk_group, + chunk_list_path, } = &*self.await?; let chunk_group = chunk_group.unwrap_or_else(|| ChunkGroupVc::from_chunk(origin_chunk.into())); @@ -63,10 +68,19 @@ impl EcmascriptChunkEvaluateVc { .iter() .map(|entry| entry.as_chunk_item(context).id()) .collect(); + let chunk_list_path = if let Some(chunk_list_path) = chunk_list_path { + let chunk_list_path = chunk_list_path.await?; + output_root + .get_path_to(&chunk_list_path) + .map(|path| path.to_string()) + } else { + None + }; Ok(EcmascriptChunkContentEvaluate { ecma_chunks_server_paths, other_chunks_server_paths, entry_modules_ids, + chunk_list_path, } .cell()) } @@ -77,4 +91,5 @@ pub(super) struct EcmascriptChunkContentEvaluate { pub ecma_chunks_server_paths: Vec, pub other_chunks_server_paths: Vec, pub entry_modules_ids: Vec, + pub chunk_list_path: Option, } diff --git a/crates/turbopack-ecmascript/src/chunk/manifest/loader_item.rs b/crates/turbopack-ecmascript/src/chunk/manifest/loader_item.rs index 6d42c59f598da..b4d4f3ded8ce0 100644 --- a/crates/turbopack-ecmascript/src/chunk/manifest/loader_item.rs +++ b/crates/turbopack-ecmascript/src/chunk/manifest/loader_item.rs @@ -6,7 +6,10 @@ use turbo_tasks::{primitives::StringVc, ValueToString}; use turbo_tasks_fs::FileSystemPathVc; use turbopack_core::{ asset::Asset, - chunk::{Chunk, ChunkItem, ChunkItemVc, ChunkableAsset, ChunkingContext, ChunkingContextVc}, + chunk::{ + Chunk, ChunkItem, ChunkItemVc, ChunkListReferenceVc, ChunkableAsset, ChunkingContext, + ChunkingContextVc, + }, ident::AssetIdentVc, reference::AssetReferencesVc, }; @@ -28,11 +31,6 @@ fn modifier() -> StringVc { StringVc::cell("loader".to_string()) } -#[turbo_tasks::function] -fn chunk_list_modifier() -> StringVc { - StringVc::cell("chunks list".to_string()) -} - /// The manifest loader item is shipped in the same chunk that uses the dynamic /// `import()` expression. Its responsibility is to load the manifest chunk from /// the server. The dynamic import has been rewritten to import this manifest @@ -59,12 +57,9 @@ impl ManifestLoaderItemVc { } #[turbo_tasks::function] - async fn chunks_list_path(self) -> Result { + async fn chunk_list_path(self) -> Result { let this = &*self.await?; - Ok(this.context.chunk_path( - this.manifest.ident().with_modifier(chunk_list_modifier()), - ".json", - )) + Ok(this.context.chunk_list_path(this.manifest.ident())) } } @@ -78,11 +73,20 @@ impl ChunkItem for ManifestLoaderItem { #[turbo_tasks::function] async fn references(self_vc: ManifestLoaderItemVc) -> Result { let this = &*self_vc.await?; - Ok(AssetReferencesVc::cell(vec![ManifestChunkAssetReference { - manifest: this.manifest, - } - .cell() - .into()])) + Ok(AssetReferencesVc::cell(vec![ + ManifestChunkAssetReference { + manifest: this.manifest, + } + .cell() + .into(), + // This creates the chunk list corresponding to the manifest chunk's chunk group. + ChunkListReferenceVc::new( + this.context.output_root(), + this.manifest.chunk_group(), + self_vc.chunk_list_path(), + ) + .into(), + ])) } } @@ -143,6 +147,7 @@ impl EcmascriptChunkItem for ManifestLoaderItem { return __turbopack_load__({chunk_server_path}).then(() => {{ return __turbopack_require__({item_id}); }}).then((chunks_paths) => {{ + __turbopack_register_chunk_list__({chunk_list_path}, chunks_paths); return Promise.all(chunks_paths.map((chunk_path) => __turbopack_load__(chunk_path))); }}).then(() => {{ return __turbopack_import__({dynamic_id}); @@ -151,7 +156,12 @@ impl EcmascriptChunkItem for ManifestLoaderItem { "#, chunk_server_path = stringify_js(chunk_server_path), item_id = stringify_js(item_id), - dynamic_id = stringify_js(dynamic_id) + dynamic_id = stringify_js(dynamic_id), + chunk_list_path = stringify_js( + output_root + .get_path_to(&*self_vc.chunk_list_path().await?) + .ok_or(anyhow!("chunk list path is not in output root"))? + ) )?; Ok(EcmascriptChunkItemContent { diff --git a/crates/turbopack-ecmascript/src/chunk/merged/content.rs b/crates/turbopack-ecmascript/src/chunk/merged/content.rs new file mode 100644 index 0000000000000..0f8dd9754bc67 --- /dev/null +++ b/crates/turbopack-ecmascript/src/chunk/merged/content.rs @@ -0,0 +1,62 @@ +use anyhow::{bail, Result}; +use turbo_tasks::TryJoinIterExt; +use turbopack_core::{ + asset::AssetContentVc, + version::{UpdateVc, VersionVc, VersionedContent, VersionedContentVc}, +}; + +use super::{ + update::update_ecmascript_merged_chunk, + version::{EcmascriptMergedChunkVersion, EcmascriptMergedChunkVersionVc}, +}; +use crate::chunk::content::EcmascriptChunkContentVc; + +/// Composite [`EcmascriptChunkContent`] that is the result of merging multiple +/// EcmaScript chunk's contents together through the +/// [`EcmascriptChunkContentMerger`]. +/// +/// [`EcmascriptChunkContentMerger`]: super::merger::EcmascriptChunkContentMerger +#[turbo_tasks::value(serialization = "none", shared)] +pub(super) struct EcmascriptMergedChunkContent { + pub contents: Vec, +} + +#[turbo_tasks::value_impl] +impl EcmascriptMergedChunkContentVc { + #[turbo_tasks::function] + pub async fn version(self) -> Result { + Ok(EcmascriptMergedChunkVersion { + versions: self + .await? + .contents + .iter() + .map(|content| async move { content.own_version().await }) + .try_join() + .await?, + } + .cell()) + } +} + +#[turbo_tasks::value_impl] +impl VersionedContent for EcmascriptMergedChunkContent { + #[turbo_tasks::function] + fn content(_self_vc: EcmascriptMergedChunkContentVc) -> Result { + bail!("EcmascriptMergedChunkContent does not have content") + } + + #[turbo_tasks::function] + fn version(self_vc: EcmascriptMergedChunkContentVc) -> VersionVc { + self_vc.version().into() + } + + #[turbo_tasks::function] + async fn update( + self_vc: EcmascriptMergedChunkContentVc, + from_version: VersionVc, + ) -> Result { + Ok(update_ecmascript_merged_chunk(self_vc, from_version) + .await? + .cell()) + } +} diff --git a/crates/turbopack-ecmascript/src/chunk/merged/merger.rs b/crates/turbopack-ecmascript/src/chunk/merged/merger.rs new file mode 100644 index 0000000000000..ddff0f06e2b67 --- /dev/null +++ b/crates/turbopack-ecmascript/src/chunk/merged/merger.rs @@ -0,0 +1,44 @@ +use anyhow::{bail, Result}; +use turbo_tasks::TryJoinIterExt; +use turbopack_core::version::{ + VersionedContentMerger, VersionedContentMergerVc, VersionedContentVc, VersionedContentsVc, +}; + +use super::content::EcmascriptMergedChunkContent; +use crate::chunk::content::EcmascriptChunkContentVc; + +/// Merges multiple [`EcmascriptChunkContent`] into a single +/// [`EcmascriptMergedChunkContent`]. This is useful for generating a single +/// update for multiple ES chunks updating all at the same time. +#[turbo_tasks::value] +pub(crate) struct EcmascriptChunkContentMerger; + +#[turbo_tasks::value_impl] +impl EcmascriptChunkContentMergerVc { + /// Creates a new [`EcmascriptChunkContentMerger`]. + #[turbo_tasks::function] + pub fn new() -> Self { + Self::cell(EcmascriptChunkContentMerger) + } +} + +#[turbo_tasks::value_impl] +impl VersionedContentMerger for EcmascriptChunkContentMerger { + #[turbo_tasks::function] + async fn merge(&self, contents: VersionedContentsVc) -> Result { + let contents = contents + .await? + .iter() + .map(|content| async move { + if let Some(content) = EcmascriptChunkContentVc::resolve_from(content).await? { + Ok(content) + } else { + bail!("expected EcmascriptChunkContentVc") + } + }) + .try_join() + .await?; + + Ok(EcmascriptMergedChunkContent { contents }.cell().into()) + } +} diff --git a/crates/turbopack-ecmascript/src/chunk/merged/mod.rs b/crates/turbopack-ecmascript/src/chunk/merged/mod.rs new file mode 100644 index 0000000000000..38431505fc60f --- /dev/null +++ b/crates/turbopack-ecmascript/src/chunk/merged/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod content; +pub(crate) mod merger; +pub(crate) mod update; +pub(crate) mod version; diff --git a/crates/turbopack-ecmascript/src/chunk/merged/update.rs b/crates/turbopack-ecmascript/src/chunk/merged/update.rs new file mode 100644 index 0000000000000..7d7601b609298 --- /dev/null +++ b/crates/turbopack-ecmascript/src/chunk/merged/update.rs @@ -0,0 +1,255 @@ +use anyhow::Result; +use indexmap::{IndexMap, IndexSet}; +use serde::Serialize; +use turbo_tasks::{primitives::JsonValueVc, TryJoinIterExt}; +use turbo_tasks_fs::rope::Rope; +use turbopack_core::{ + chunk::{ModuleId, ModuleIdReadRef}, + code_builder::CodeReadRef, + version::{PartialUpdate, TotalUpdate, Update, VersionVc}, +}; + +use super::{content::EcmascriptMergedChunkContentVc, version::EcmascriptMergedChunkVersionVc}; +use crate::chunk::{ + update::{update_ecmascript_chunk, EcmascriptChunkUpdate}, + version::EcmascriptChunkVersionReadRef, +}; + +#[derive(Serialize, Default)] +#[serde(tag = "type", rename_all = "camelCase")] +struct EcmascriptMergedUpdate<'a> { + /// A map from module id to latest module entry. + #[serde(skip_serializing_if = "IndexMap::is_empty")] + entries: IndexMap, + /// A map from chunk path to the chunk update. + #[serde(skip_serializing_if = "IndexMap::is_empty")] + chunks: IndexMap<&'a str, EcmascriptMergedChunkUpdate>, +} + +impl EcmascriptMergedUpdate<'_> { + fn is_empty(&self) -> bool { + self.entries.is_empty() && self.chunks.is_empty() + } +} + +#[derive(Serialize)] +#[serde(tag = "type", rename_all = "camelCase")] +enum EcmascriptMergedChunkUpdate { + Added(EcmascriptMergedChunkAdded), + Deleted(EcmascriptMergedChunkDeleted), + Partial(EcmascriptMergedChunkPartial), +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +struct EcmascriptMergedChunkAdded { + #[serde(skip_serializing_if = "IndexSet::is_empty")] + modules: IndexSet, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +struct EcmascriptMergedChunkDeleted { + // Technically, this is redundant, since the client will already know all + // modules in the chunk from the previous version. However, it's useful for + // merging updates without access to an initial state. + #[serde(skip_serializing_if = "IndexSet::is_empty")] + modules: IndexSet, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +struct EcmascriptMergedChunkPartial { + #[serde(skip_serializing_if = "IndexSet::is_empty")] + added: IndexSet, + #[serde(skip_serializing_if = "IndexSet::is_empty")] + deleted: IndexSet, +} + +#[derive(Serialize)] +struct EcmascriptModuleEntry { + code: Rope, + url: String, + map: Option, +} + +impl EcmascriptModuleEntry { + fn new(id: &ModuleId, code: CodeReadRef, chunk_path: &str) -> Self { + /// serde_qs can't serialize a lone enum when it's [serde::untagged]. + #[derive(Serialize)] + struct Id<'a> { + id: &'a ModuleId, + } + let id = serde_qs::to_string(&Id { id }).unwrap(); + EcmascriptModuleEntry { + // Cloning a rope is cheap. + code: code.source_code().clone(), + url: format!("/{}?{}", chunk_path, &id), + map: code + .has_source_map() + .then(|| format!("/__turbopack_sourcemap__/{}.map?{}", chunk_path, &id)), + } + } +} + +/// Helper structure to get a module's hash from multiple different chunk +/// versions, without having to actually merge the versions into a single +/// hashmap, which would be expensive. +struct MergedModuleMap { + versions: Vec, +} + +impl MergedModuleMap { + /// Creates a new `MergedModuleMap` from the given versions. + fn new(versions: Vec) -> Self { + Self { versions } + } + + /// Returns the hash of the module with the given id, or `None` if the + /// module is not present in any of the versions. + fn get(&self, id: &ModuleId) -> Option { + for version in &self.versions { + if let Some(hash) = version.module_factories_hashes.get(id) { + return Some(*hash); + } + } + None + } +} + +pub(super) async fn update_ecmascript_merged_chunk( + content: EcmascriptMergedChunkContentVc, + from_version: VersionVc, +) -> Result { + let to_merged_version = content.version(); + let from_merged_version = + if let Some(from) = EcmascriptMergedChunkVersionVc::resolve_from(from_version).await? { + from + } else { + // It's likely `from_version` is `NotFoundVersion`. + return Ok(Update::Total(TotalUpdate { + to: to_merged_version.into(), + })); + }; + + let to = to_merged_version.await?; + let from = from_merged_version.await?; + + // When to and from point to the same value we can skip comparing them. + // This will happen since `TraitRef::cell` will not clone the value, + // but only make the cell point to the same immutable value (Arc). + if from.ptr_eq(&to) { + return Ok(Update::None); + } + + let mut from_versions_by_chunk_path: IndexMap<_, _> = from + .versions + .iter() + .map(|version| (&*version.chunk_server_path, version)) + .collect(); + + let merged_module_map = MergedModuleMap::new(from.versions.iter().cloned().collect()); + + let content = content.await?; + let to_contents = content + .contents + .iter() + .map(|content| async move { Ok((*content, content.await?)) }) + .try_join() + .await?; + + let mut merged_update = EcmascriptMergedUpdate::default(); + + for (content, content_ref) in &to_contents { + let Some(chunk_server_path) = content_ref + .output_root + .get_path_to(&content_ref.chunk_path) else { + continue; + }; + + let chunk_update = if let Some(from_version) = + from_versions_by_chunk_path.remove(chunk_server_path) + { + // The chunk was present in the previous version, so we must update it. + let update = update_ecmascript_chunk(*content, from_version).await?; + + match update { + EcmascriptChunkUpdate::None => { + // Nothing changed, so we can skip this chunk. + continue; + } + EcmascriptChunkUpdate::Partial(chunk_partial) => { + // The chunk was updated. + let mut partial = EcmascriptMergedChunkPartial::default(); + + for (module_id, (module_hash, module_code)) in chunk_partial.added { + partial.added.insert(module_id.clone()); + + if merged_module_map.get(&module_id) != Some(module_hash) { + let entry = EcmascriptModuleEntry::new( + &*module_id, + module_code.clone(), + chunk_server_path, + ); + merged_update.entries.insert(module_id, entry); + } + } + + partial.deleted.extend(chunk_partial.deleted.into_keys()); + + for (module_id, module_code) in chunk_partial.modified { + let entry = + EcmascriptModuleEntry::new(&*module_id, module_code, chunk_server_path); + merged_update.entries.insert(module_id, entry); + } + + EcmascriptMergedChunkUpdate::Partial(partial) + } + } + } else { + // The chunk was added in this version. + let mut added = EcmascriptMergedChunkAdded::default(); + + for entry in &content_ref.module_factories { + added.modules.insert(entry.id.clone()); + + if merged_module_map.get(&entry.id) != Some(entry.hash) { + merged_update.entries.insert( + entry.id.clone(), + EcmascriptModuleEntry::new( + &entry.id, + entry.code.clone(), + chunk_server_path, + ), + ); + } + } + + EcmascriptMergedChunkUpdate::Added(added) + }; + + merged_update.chunks.insert(chunk_server_path, chunk_update); + } + + // Deleted chunks. + for (chunk_server_path, chunk_version) in from_versions_by_chunk_path { + let hashes = &chunk_version.module_factories_hashes; + merged_update.chunks.insert( + chunk_server_path, + EcmascriptMergedChunkUpdate::Deleted(EcmascriptMergedChunkDeleted { + modules: hashes.keys().cloned().collect(), + }), + ); + } + + let update = if merged_update.is_empty() { + Update::None + } else { + Update::Partial(PartialUpdate { + to: to_merged_version.into(), + instruction: JsonValueVc::cell(serde_json::to_value(&merged_update)?), + }) + }; + + Ok(update) +} diff --git a/crates/turbopack-ecmascript/src/chunk/merged/version.rs b/crates/turbopack-ecmascript/src/chunk/merged/version.rs new file mode 100644 index 0000000000000..8e9684d8ed504 --- /dev/null +++ b/crates/turbopack-ecmascript/src/chunk/merged/version.rs @@ -0,0 +1,39 @@ +use anyhow::Result; +use turbo_tasks::{primitives::StringVc, ReadRef, TryJoinIterExt}; +use turbo_tasks_hash::{encode_hex, Xxh3Hash64Hasher}; +use turbopack_core::version::{Version, VersionVc}; + +use crate::chunk::version::EcmascriptChunkVersionReadRef; + +/// The version of a [`super::content::EcmascriptMergedChunkContent`]. This is +/// essentially a composite [`EcmascriptChunkVersion`]. +#[turbo_tasks::value(serialization = "none", shared)] +pub(super) struct EcmascriptMergedChunkVersion { + #[turbo_tasks(trace_ignore)] + pub(super) versions: Vec, +} + +#[turbo_tasks::value_impl] +impl Version for EcmascriptMergedChunkVersion { + #[turbo_tasks::function] + async fn id(&self) -> Result { + let mut hasher = Xxh3Hash64Hasher::new(); + hasher.write_value(self.versions.len()); + let sorted_ids = { + let mut sorted_ids = self + .versions + .iter() + .map(|version| async move { ReadRef::cell(version.clone()).id().await }) + .try_join() + .await?; + sorted_ids.sort(); + sorted_ids + }; + for id in sorted_ids { + hasher.write_value(id); + } + let hash = hasher.finish(); + let hex_hash = encode_hex(hash); + Ok(StringVc::cell(hex_hash)) + } +} diff --git a/crates/turbopack-ecmascript/src/chunk/mod.rs b/crates/turbopack-ecmascript/src/chunk/mod.rs index e8e18f1dee172..76a7f742a810e 100644 --- a/crates/turbopack-ecmascript/src/chunk/mod.rs +++ b/crates/turbopack-ecmascript/src/chunk/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod context; pub(crate) mod evaluate; pub(crate) mod item; pub(crate) mod manifest; +pub(crate) mod merged; pub(crate) mod module_factory; pub(crate) mod optimize; pub(crate) mod placeable; @@ -24,8 +25,8 @@ use turbopack_core::{ asset::{Asset, AssetContentVc, AssetVc}, chunk::{ optimize::{ChunkOptimizerVc, OptimizableChunk, OptimizableChunkVc}, - Chunk, ChunkGroupReferenceVc, ChunkItem, ChunkReferenceVc, ChunkVc, ChunkingContext, - ChunkingContextVc, + Chunk, ChunkGroupReferenceVc, ChunkGroupVc, ChunkItem, ChunkListReferenceVc, + ChunkReferenceVc, ChunkVc, ChunkingContext, ChunkingContextVc, }, ident::{AssetIdent, AssetIdentVc}, introspect::{ @@ -111,6 +112,7 @@ impl EcmascriptChunkVc { EcmascriptChunkEvaluate { evaluate_entries: entries, chunk_group: None, + chunk_list_path: Some(context.chunk_list_path(main_entry.ident())), } .cell(), ), @@ -393,6 +395,25 @@ impl Asset for EcmascriptChunk { for chunk_group in content.async_chunk_groups.iter() { references.push(ChunkGroupReferenceVc::new(*chunk_group).into()); } + if let Some(evaluate) = this.evaluate { + let EcmascriptChunkEvaluate { + chunk_list_path, + chunk_group, + .. + } = *evaluate.await?; + if let Some(chunk_list_path) = chunk_list_path { + let chunk_group = + chunk_group.unwrap_or_else(|| ChunkGroupVc::from_chunk(self_vc.into())); + references.push( + ChunkListReferenceVc::new( + this.context.output_root(), + chunk_group, + chunk_list_path, + ) + .into(), + ); + } + } references.push(EcmascriptChunkSourceMapAssetReferenceVc::new(self_vc).into()); Ok(AssetReferencesVc::cell(references)) diff --git a/crates/turbopack-ecmascript/src/chunk/module_factory.rs b/crates/turbopack-ecmascript/src/chunk/module_factory.rs index 8fb82390491aa..11bd34bd442cf 100644 --- a/crates/turbopack-ecmascript/src/chunk/module_factory.rs +++ b/crates/turbopack-ecmascript/src/chunk/module_factory.rs @@ -17,6 +17,7 @@ pub(super) async fn module_factory(content: EcmascriptChunkItemContentVc) -> Res "v: __turbopack_export_value__", "c: __turbopack_cache__", "l: __turbopack_load__", + "k: __turbopack_register_chunk_list__", "j: __turbopack_cjs__", "p: process", "g: global", diff --git a/crates/turbopack-ecmascript/src/chunk/optimize.rs b/crates/turbopack-ecmascript/src/chunk/optimize.rs index a3e93bf9abc8a..19725d67878f2 100644 --- a/crates/turbopack-ecmascript/src/chunk/optimize.rs +++ b/crates/turbopack-ecmascript/src/chunk/optimize.rs @@ -399,6 +399,7 @@ async fn optimize_ecmascript( EcmascriptChunkEvaluate { evaluate_entries: evaluate.evaluate_entries, chunk_group: Some(chunk_group), + chunk_list_path: evaluate.chunk_list_path, } .cell(), ), diff --git a/crates/turbopack-ecmascript/src/chunk/snapshot.rs b/crates/turbopack-ecmascript/src/chunk/snapshot.rs index 0f0359da427fe..85da26c2d21ae 100644 --- a/crates/turbopack-ecmascript/src/chunk/snapshot.rs +++ b/crates/turbopack-ecmascript/src/chunk/snapshot.rs @@ -2,7 +2,6 @@ use std::{fmt::Write, io::Write as _, slice::Iter}; use anyhow::Result; use turbo_tasks::{primitives::StringVc, TryJoinIterExt, ValueToString}; -use turbo_tasks_fs::rope::Rope; use turbo_tasks_hash::hash_xxh3_hash64; use turbopack_core::{ chunk::{ChunkItem, ModuleId, ModuleIdReadRef}, @@ -115,10 +114,6 @@ impl EcmascriptChunkContentEntry { pub fn code(&self) -> &Code { &self.code } - - pub fn source_code(&self) -> &Rope { - self.code.source_code() - } } #[turbo_tasks::value_impl] diff --git a/crates/turbopack-ecmascript/src/chunk/update.rs b/crates/turbopack-ecmascript/src/chunk/update.rs index 93b891ea660cb..80a08a767ffd2 100644 --- a/crates/turbopack-ecmascript/src/chunk/update.rs +++ b/crates/turbopack-ecmascript/src/chunk/update.rs @@ -1,121 +1,72 @@ use anyhow::Result; -use indexmap::{IndexMap, IndexSet}; -use serde::Serialize; -use turbo_tasks::primitives::JsonValueVc; -use turbo_tasks_fs::rope::Rope; -use turbopack_core::{ - chunk::ModuleId, - version::{PartialUpdate, TotalUpdate, Update, UpdateVc, VersionVc}, -}; +use indexmap::IndexMap; +use turbopack_core::{chunk::ModuleIdReadRef, code_builder::CodeReadRef}; -use super::{ - content::EcmascriptChunkContentVc, snapshot::EcmascriptChunkContentEntry, - version::EcmascriptChunkVersionVc, -}; +use super::{content::EcmascriptChunkContentVc, version::EcmascriptChunkVersionReadRef}; -#[derive(Serialize)] -#[serde(tag = "type")] -struct EcmascriptChunkUpdate<'a> { - added: IndexMap<&'a ModuleId, HmrUpdateEntry<'a>>, - modified: IndexMap<&'a ModuleId, HmrUpdateEntry<'a>>, - deleted: IndexSet<&'a ModuleId>, +pub(super) enum EcmascriptChunkUpdate { + None, + Partial(EcmascriptChunkPartialUpdate), +} + +pub(super) struct EcmascriptChunkPartialUpdate { + pub added: IndexMap, + pub deleted: IndexMap, + pub modified: IndexMap, } -#[turbo_tasks::function] pub(super) async fn update_ecmascript_chunk( content: EcmascriptChunkContentVc, - from_version: VersionVc, -) -> Result { - let to_version = content.version(); - let from_version = - if let Some(from) = EcmascriptChunkVersionVc::resolve_from(from_version).await? { - from - } else { - return Ok(Update::Total(TotalUpdate { - to: to_version.into(), - }) - .cell()); - }; - - let to = to_version.await?; - let from = from_version.await?; + from: &EcmascriptChunkVersionReadRef, +) -> Result { + let to = content.own_version().await?; // When to and from point to the same value we can skip comparing them. - // This will happen since `cell_local` will not clone the value, but only make - // the local cell point to the same immutable value (Arc). + // This will happen since `TraitRef::cell` will not clone the value, + // but only make the cell point to the same immutable value (Arc). if from.ptr_eq(&to) { - return Ok(Update::None.cell()); + return Ok(EcmascriptChunkUpdate::None); } let content = content.await?; - let chunk_path = &content.chunk_path.path; // TODO(alexkirsz) This should probably be stored as a HashMap already. let mut module_factories: IndexMap<_, _> = content .module_factories .iter() - .map(|entry| (entry.id(), entry)) + .map(|entry| (entry.id.clone(), entry)) .collect(); - let mut added = IndexMap::new(); - let mut modified = IndexMap::new(); - let mut deleted = IndexSet::new(); + let mut added = IndexMap::default(); + let mut modified = IndexMap::default(); + let mut deleted = IndexMap::default(); - for (id, hash) in &from.module_factories_hashes { - let id = &**id; + for (id, from_hash) in &from.module_factories_hashes { + let id = id; if let Some(entry) = module_factories.remove(id) { - if entry.hash != *hash { - modified.insert(id, HmrUpdateEntry::new(entry, chunk_path)); + if entry.hash != *from_hash { + modified.insert(id.clone(), entry.code.clone()); } } else { - deleted.insert(id); + deleted.insert(id.clone(), *from_hash); } } // Remaining entries are added for (id, entry) in module_factories { - added.insert(id, HmrUpdateEntry::new(entry, chunk_path)); + if !from.module_factories_hashes.contains_key(&id) { + added.insert(id, (entry.hash, entry.code.clone())); + } } let update = if added.is_empty() && modified.is_empty() && deleted.is_empty() { - Update::None + EcmascriptChunkUpdate::None } else { - let chunk_update = EcmascriptChunkUpdate { + EcmascriptChunkUpdate::Partial(EcmascriptChunkPartialUpdate { added, modified, deleted, - }; - - Update::Partial(PartialUpdate { - to: to_version.into(), - instruction: JsonValueVc::cell(serde_json::to_value(&chunk_update)?), }) }; - Ok(update.into()) -} - -#[derive(serde::Serialize)] -struct HmrUpdateEntry<'a> { - code: &'a Rope, - url: String, - map: Option, -} - -impl<'a> HmrUpdateEntry<'a> { - fn new(entry: &'a EcmascriptChunkContentEntry, chunk_path: &str) -> Self { - /// serde_qs can't serialize a lone enum when it's [serde::untagged]. - #[derive(Serialize)] - struct Id<'a> { - id: &'a ModuleId, - } - let id = serde_qs::to_string(&Id { id: &entry.id }).unwrap(); - HmrUpdateEntry { - code: entry.source_code(), - url: format!("/{}?{}", chunk_path, &id), - map: entry - .code - .has_source_map() - .then(|| format!("/__turbopack_sourcemap__/{}.map?{}", chunk_path, &id)), - } - } + Ok(update) } diff --git a/crates/turbopack-ecmascript/src/chunk_group_files_asset.rs b/crates/turbopack-ecmascript/src/chunk_group_files_asset.rs index 285c83e3e7e41..6fd59285a043e 100644 --- a/crates/turbopack-ecmascript/src/chunk_group_files_asset.rs +++ b/crates/turbopack-ecmascript/src/chunk_group_files_asset.rs @@ -5,7 +5,7 @@ use turbopack_core::{ asset::{Asset, AssetContentVc, AssetVc}, chunk::{ Chunk, ChunkGroupVc, ChunkItem, ChunkItemVc, ChunkReferenceVc, ChunkVc, ChunkableAsset, - ChunkableAssetVc, ChunkingContextVc, ChunksVc, + ChunkableAssetVc, ChunkingContext, ChunkingContextVc, }, ident::AssetIdentVc, reference::AssetReferencesVc, @@ -33,13 +33,14 @@ pub struct ChunkGroupFilesAsset { pub asset: ChunkableAssetVc, pub chunking_context: ChunkingContextVc, pub base_path: FileSystemPathVc, + pub server_root: FileSystemPathVc, pub runtime_entries: Option, } #[turbo_tasks::value_impl] impl ChunkGroupFilesAssetVc { #[turbo_tasks::function] - async fn chunks(self) -> Result { + async fn chunk_group(self) -> Result { let this = self.await?; let chunk_group = if let Some(ecma) = EcmascriptModuleAssetVc::resolve_from(this.asset).await? { @@ -49,7 +50,13 @@ impl ChunkGroupFilesAssetVc { } else { ChunkGroupVc::from_asset(this.asset, this.chunking_context) }; - Ok(chunk_group.chunks()) + Ok(chunk_group) + } + + #[turbo_tasks::function] + async fn chunk_list_path(self) -> Result { + let this = &*self.await?; + Ok(this.chunking_context.chunk_list_path(this.asset.ident())) } } @@ -118,7 +125,7 @@ impl EcmascriptChunkItem for ChunkGroupFilesChunkItem { #[turbo_tasks::function] async fn content(&self) -> Result { - let chunks = self.inner.chunks(); + let chunks = self.inner.chunk_group().chunks(); let base_path = self.inner.await?.base_path.await?; let chunks_paths = chunks .await? @@ -151,16 +158,17 @@ impl ChunkItem for ChunkGroupFilesChunkItem { #[turbo_tasks::function] async fn references(&self) -> Result { - let chunks = self.inner.chunks(); - - Ok(AssetReferencesVc::cell( - chunks - .await? - .iter() - .copied() - .map(ChunkReferenceVc::new) - .map(Into::into) - .collect(), - )) + let chunk_group = self.inner.chunk_group(); + let chunks = chunk_group.chunks(); + + let references: Vec<_> = chunks + .await? + .iter() + .copied() + .map(ChunkReferenceVc::new) + .map(Into::into) + .collect(); + + Ok(AssetReferencesVc::cell(references)) } } diff --git a/crates/turbopack-test-utils/src/snapshot.rs b/crates/turbopack-test-utils/src/snapshot.rs index c69ad42a6a493..8f20ab4b936cb 100644 --- a/crates/turbopack-test-utils/src/snapshot.rs +++ b/crates/turbopack-test-utils/src/snapshot.rs @@ -102,32 +102,33 @@ pub async fn diff(path: FileSystemPathVc, actual: AssetContentVc) -> Result<()> let path_str = &path.await?.path; let expected = path.read().into(); - let actual = match get_contents(actual, path).await? { - Some(s) => s, - None => bail!("could not generate {} contents", path_str), - }; + let actual = get_contents(actual, path).await?; let expected = get_contents(expected, path).await?; - if Some(&actual) != expected.as_ref() { - if *UPDATE { - let content = File::from(actual).into(); - path.write(content).await?; - println!("updated contents of {}", path_str); - } else { - if expected.is_none() { - eprintln!("new file {path_str} detected:"); + if actual != expected { + if let Some(actual) = actual { + if *UPDATE { + let content = File::from(actual).into(); + path.write(content).await?; + println!("updated contents of {}", path_str); } else { - eprintln!("contents of {path_str} did not match:"); + if expected.is_none() { + eprintln!("new file {path_str} detected:"); + } else { + eprintln!("contents of {path_str} did not match:"); + } + let expected = expected.unwrap_or_default(); + let diff = TextDiff::from_lines(&expected, &actual); + eprintln!( + "{}", + diff.unified_diff() + .context_radius(3) + .header("expected", "actual") + ); + bail!("contents of {path_str} did not match"); } - let expected = expected.unwrap_or_default(); - let diff = TextDiff::from_lines(&expected, &actual); - eprintln!( - "{}", - diff.unified_diff() - .context_radius(3) - .header("expected", "actual") - ); - bail!("contents of {path_str} did not match"); + } else { + bail!("{path_str} was not generated"); } } diff --git a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/20803_foo_index.js b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/20803_foo_index.js index 8936f5ee92f3e..5c8cf0cff709d 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/20803_foo_index.js +++ b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/20803_foo_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/20803_foo_index.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/node_modules/foo/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/node_modules/foo/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "foo": ()=>foo diff --git a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js index 4c1a8c94a8439..4b84efd15d5e0 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js +++ b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$async_chunk$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/node_modules/foo/index.js (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; diff --git a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import_93c5e8.js b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import_93c5e8.js index cc76de64f9ddf..e38e1d37d0fba 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import_93c5e8.js +++ b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import_93c5e8.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import_93c5e8.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__([ "output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js", diff --git a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js new file mode 100644 index 0000000000000..e0b0b4edce2c7 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js @@ -0,0 +1,1469 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_export_value__((__turbopack_import__) => { + return __turbopack_load__("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import_93c5e8.js").then(() => { + return __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk)"); + }).then((chunks_paths) => { + __turbopack_register_chunk_list__("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js_d744dd._.json", chunks_paths); + return Promise.all(chunks_paths.map((chunk_path) => __turbopack_load__(chunk_path))); + }).then(() => { + return __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript)"); + }); +}); + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +__turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk, loader)")(__turbopack_import__).then(({ foo })=>{ + foo(true); +}); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js.map b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js.map new file mode 100644 index 0000000000000..f8106d6faf06c --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js"],"sourcesContent":["import(\"./import\").then(({ foo }) => {\n foo(true);\n});\n"],"names":[],"mappings":"AAAA,sKAAmB,IAAI,CAAC,CAAC,EAAE,IAAG,EAAE,GAAK;IACnC,IAAI,IAAI;AACV"}}, + {"offset": {"line": 21, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js index 64981fc659c69..9fdda376a123b 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js +++ b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js @@ -1,11 +1,12 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__((__turbopack_import__) => { return __turbopack_load__("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import_93c5e8.js").then(() => { return __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk)"); }).then((chunks_paths) => { + __turbopack_register_chunk_list__("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_import.js_d744dd._.json", chunks_paths); return Promise.all(chunks_paths.map((chunk_path) => __turbopack_load__(chunk_path))); }).then(() => { return __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript)"); @@ -13,15 +14,16 @@ __turbopack_export_value__((__turbopack_import__) => { }); })()), -"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/import.js (ecmascript, manifest chunk, loader)")(__turbopack_import__).then(({ foo })=>{ foo(true); }); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_580957.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js (ecmascript)"); } ]); @@ -33,7 +35,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -64,6 +66,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -88,8 +149,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -141,6 +205,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -415,6 +502,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -556,7 +644,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -567,18 +655,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -595,7 +685,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -627,35 +717,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -668,10 +767,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -705,24 +809,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -745,22 +852,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -850,17 +1113,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -963,10 +1244,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -991,18 +1282,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1012,18 +1367,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1044,6 +1438,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1051,7 +1446,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js.map b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js.map index 1e02192df84b2..f8106d6faf06c 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js.map +++ b/crates/turbopack-tests/tests/snapshot/basic/async_chunk/output/crates_turbopack-tests_tests_snapshot_basic_async_chunk_input_index_da3760.js.map @@ -1,6 +1,6 @@ { "version": 3, "sections": [ - {"offset": {"line": 17, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js"],"sourcesContent":["import(\"./import\").then(({ foo }) => {\n foo(true);\n});\n"],"names":[],"mappings":"AAAA,sKAAmB,IAAI,CAAC,CAAC,EAAE,IAAG,EAAE,GAAK;IACnC,IAAI,IAAI;AACV"}}, - {"offset": {"line": 20, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/basic/async_chunk/input/index.js"],"sourcesContent":["import(\"./import\").then(({ foo }) => {\n foo(true);\n});\n"],"names":[],"mappings":"AAAA,sKAAmB,IAAI,CAAC,CAAC,EAAE,IAAG,EAAE,GAAK;IACnC,IAAI,IAAI;AACV"}}, + {"offset": {"line": 21, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] } \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/chunked/output/39e84_foo_index.js b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/39e84_foo_index.js index ef5532ab97bc7..f548771395b8c 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/chunked/output/39e84_foo_index.js +++ b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/39e84_foo_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/39e84_foo_index.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/node_modules/foo/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/node_modules/foo/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "foo": ()=>foo diff --git a/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js new file mode 100644 index 0000000000000..87538d9dfddf4 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js @@ -0,0 +1,1456 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$chunked$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/node_modules/foo/index.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$chunked$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__["foo"](true); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/39e84_foo_index.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index.js_f5704b._.json", ["output/39e84_foo_index.js"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js.map b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js.map new file mode 100644 index 0000000000000..366fb314a6311 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/basic/chunked/input/index.js"],"sourcesContent":["import { foo } from \"foo\";\n\nfoo(true);\n"],"names":[],"mappings":";;;AAEA,iMAAI,IAAI"}}, + {"offset": {"line": 8, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_e8535e.js b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_e8535e.js index 394b22ddc1ff0..5c8ce976d6d6e 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_e8535e.js +++ b/crates/turbopack-tests/tests/snapshot/basic/chunked/output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_e8535e.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_e8535e.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$chunked$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/node_modules/foo/index.js (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -8,8 +8,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$chunked$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__["foo"](true); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js") && loadedChunks.has("output/39e84_foo_index.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js") && loadedChunks.has("output/39e84_foo_index.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_basic_chunked_input_index_1d9ecb.js","output/39e84_foo_index.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/basic/chunked/input/index.js (ecmascript)"); } ]); @@ -21,7 +22,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -52,6 +53,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -76,8 +136,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -129,6 +192,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -403,6 +489,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -544,7 +631,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -555,18 +642,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -583,7 +672,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -615,35 +704,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -656,10 +754,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -693,24 +796,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -733,22 +839,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -838,17 +1100,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -951,10 +1231,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -979,18 +1269,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1000,18 +1354,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1032,6 +1425,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1039,7 +1433,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js new file mode 100644 index 0000000000000..2f0b46a068428 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js @@ -0,0 +1,1456 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$shebang$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/node_modules/foo/index.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$shebang$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__["foo"](true); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/d1787_foo_index.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index.js_f5704b._.json", ["output/d1787_foo_index.js"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js.map b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js.map new file mode 100644 index 0000000000000..dd1aee300d2f6 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/basic/shebang/input/index.js"],"sourcesContent":["#!/usr/bin/env node\n\nimport { foo } from \"foo\";\n\nfoo(true);\n"],"names":[],"mappings":";;;AAIA,iMAAI,IAAI"}}, + {"offset": {"line": 8, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_a8bbcc.js b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_a8bbcc.js index edd948886d2d9..859359be317e8 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_a8bbcc.js +++ b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_a8bbcc.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_a8bbcc.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$shebang$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/node_modules/foo/index.js (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -8,8 +8,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$basic$2f$shebang$2f$input$2f$node_modules$2f$foo$2f$index$2e$js__$28$ecmascript$29__["foo"](true); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js") && loadedChunks.has("output/d1787_foo_index.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js") && loadedChunks.has("output/d1787_foo_index.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_basic_shebang_input_index_718090.js","output/d1787_foo_index.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/index.js (ecmascript)"); } ]); @@ -21,7 +22,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -52,6 +53,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -76,8 +136,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -129,6 +192,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -403,6 +489,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -544,7 +631,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -555,18 +642,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -583,7 +672,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -615,35 +704,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -656,10 +754,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -693,24 +796,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -733,22 +839,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -838,17 +1100,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -951,10 +1231,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -979,18 +1269,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1000,18 +1354,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1032,6 +1425,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1039,7 +1433,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/basic/shebang/output/d1787_foo_index.js b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/d1787_foo_index.js index 807d9d378e5a7..b8a18c6ca079f 100644 --- a/crates/turbopack-tests/tests/snapshot/basic/shebang/output/d1787_foo_index.js +++ b/crates/turbopack-tests/tests/snapshot/basic/shebang/output/d1787_foo_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/d1787_foo_index.js", { -"[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/node_modules/foo/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/basic/shebang/input/node_modules/foo/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "foo": ()=>foo diff --git a/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js b/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js new file mode 100644 index 0000000000000..caa4327b5aaa7 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js @@ -0,0 +1,1477 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/comptime/define/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +if ("TURBOPACK compile-time truthy", 1) { + console.log('DEFINED_VALUE'); +} +if ("TURBOPACK compile-time truthy", 1) { + console.log('DEFINED_VALUE'); +} +if ("TURBOPACK compile-time truthy", 1) { + console.log('A.VERY.LONG.DEFINED.VALUE'); +} +if ("TURBOPACK compile-time truthy", 1) { + console.log('something'); +} +if ("TURBOPACK compile-time falsy", 0) { + "TURBOPACK unreachable"; +} +var p = process; +console.log(A.VERY.LONG.DEFINED.VALUE); +console.log(DEFINED_VALUE); +console.log(p.env.NODE_ENV); +if ("TURBOPACK compile-time falsy", 0) { + "TURBOPACK unreachable"; +} +p.env.NODE_ENV == 'production' ? console.log('production') : console.log('development'); +p.env.NODE_ENV != 'production' && console.log('development'); +p.env.NODE_ENV == 'production' && console.log('production'); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/comptime/define/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js.map b/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js.map new file mode 100644 index 0000000000000..a3fac3e0e9d61 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/comptime/define/input/index.js"],"sourcesContent":["if (DEFINED_VALUE) {\n console.log('DEFINED_VALUE');\n}\n\nif (DEFINED_TRUE) {\n console.log('DEFINED_VALUE');\n}\n\nif (A.VERY.LONG.DEFINED.VALUE) {\n console.log('A.VERY.LONG.DEFINED.VALUE');\n}\n\nif (process.env.NODE_ENV) {\n console.log('something');\n}\n\nif (process.env.NODE_ENV === 'production') {\n console.log('production');\n}\n\nvar p = process;\n\n// TODO: replacement is not implemented yet\nconsole.log(A.VERY.LONG.DEFINED.VALUE);\nconsole.log(DEFINED_VALUE);\nconsole.log(p.env.NODE_ENV);\n\nif (p.env.NODE_ENV === 'production') {\n console.log('production');\n}\n\n// TODO tenary is not implemented yet\np.env.NODE_ENV == 'production' ? console.log('production') : console.log('development');\n\n// TODO short-circuit is not implemented yet\np.env.NODE_ENV != 'production' && console.log('development');\np.env.NODE_ENV == 'production' && console.log('production');\n"],"names":[],"mappings":"AAAA,wCAAmB;IACjB,QAAQ,GAAG,CAAC;AACd,CAAC;AAED,wCAAkB;IAChB,QAAQ,GAAG,CAAC;AACd,CAAC;AAED,wCAA+B;IAC7B,QAAQ,GAAG,CAAC;AACd,CAAC;AAED,wCAA0B;IACxB,QAAQ,GAAG,CAAC;AACd,CAAC;AAED;;CAEC;AAED,IAAI,IAAI;AAGR,QAAQ,GAAG,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK;AACrC,QAAQ,GAAG,CAAC;AACZ,QAAQ,GAAG,CAAC,EAAE,GAAG,CAAC,QAAQ;AAE1B;;CAEC;AAGD,EAAE,GAAG,CAAC,QAAQ,IAAI,eAAe,QAAQ,GAAG,CAAC,gBAAgB,QAAQ,GAAG,CAAC,cAAc;AAGvF,EAAE,GAAG,CAAC,QAAQ,IAAI,gBAAgB,QAAQ,GAAG,CAAC;AAC9C,EAAE,GAAG,CAAC,QAAQ,IAAI,gBAAgB,QAAQ,GAAG,CAAC"}}, + {"offset": {"line": 29, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_f522f3.js b/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_f522f3.js index f33a8213037fb..8c6e09f077940 100644 --- a/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_f522f3.js +++ b/crates/turbopack-tests/tests/snapshot/comptime/define/output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_f522f3.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_f522f3.js", { -"[project]/crates/turbopack-tests/tests/snapshot/comptime/define/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/comptime/define/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { if ("TURBOPACK compile-time truthy", 1) { console.log('DEFINED_VALUE'); @@ -29,8 +29,9 @@ p.env.NODE_ENV != 'production' && console.log('development'); p.env.NODE_ENV == 'production' && console.log('production'); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_comptime_define_input_index_68d56d.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/comptime/define/input/index.js (ecmascript)"); } ]); @@ -42,7 +43,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -73,6 +74,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -97,8 +157,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -150,6 +213,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -424,6 +510,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -565,7 +652,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -576,18 +663,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -604,7 +693,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -636,35 +725,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -677,10 +775,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -714,24 +817,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -754,22 +860,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -859,17 +1121,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -972,10 +1252,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -1000,18 +1290,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1021,18 +1375,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1053,6 +1446,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1060,7 +1454,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_24fbf6.js b/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_24fbf6.js index 87fcc749c36c9..57fabe450701b 100644 --- a/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_24fbf6.js +++ b/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_24fbf6.js @@ -1,12 +1,13 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_24fbf6.js", { -"[project]/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { ; }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js","output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index.css"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/input/index.js (ecmascript)"); } ]); @@ -18,7 +19,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -49,6 +50,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -73,8 +133,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -126,6 +189,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -400,6 +486,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -541,7 +628,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -552,18 +639,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -580,7 +669,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -612,35 +701,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -653,10 +751,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -690,24 +793,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -730,22 +836,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -835,17 +1097,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -948,10 +1228,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -976,18 +1266,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -997,18 +1351,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1029,6 +1422,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1036,7 +1430,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js b/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js new file mode 100644 index 0000000000000..f8e58e7d4359c --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js @@ -0,0 +1,1453 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +; + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index.css"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js.map b/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js.map new file mode 100644 index 0000000000000..0d68ee697ae09 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/css/absolute-uri-import/output/crates_turbopack-tests_tests_snapshot_css_absolute-uri-import_input_index_73a15e.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 5, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/css/css/output/8697f_foo_style.module.css_354da7._.js b/crates/turbopack-tests/tests/snapshot/css/css/output/8697f_foo_style.module.css_354da7._.js index c0d9dea4ec77f..3ff660ac11b99 100644 --- a/crates/turbopack-tests/tests/snapshot/css/css/output/8697f_foo_style.module.css_354da7._.js +++ b/crates/turbopack-tests/tests/snapshot/css/css/output/8697f_foo_style.module.css_354da7._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/8697f_foo_style.module.css_354da7._.js", { -"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__({ "foo-module-style": "foo-module-style__style__abf9e738", diff --git a/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_47659c.js b/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_47659c.js index 76ce5d581a998..921e31bf882d5 100644 --- a/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_47659c.js +++ b/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_47659c.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_css_css_input_index_47659c.js", { -"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/style.module.css (css, css module)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/style.module.css (css, css module)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__({ "another-composed-module-style": "another-composed-module-style__style__9bcf751c" + " " + __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)")["foo-module-style"], @@ -10,7 +10,7 @@ __turbopack_export_value__({ }); })()), -"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$node_modules$2f$foo$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)"); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/style.module.css (css, css module)"); @@ -23,8 +23,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__["default"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$node_modules$2f$foo$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__["default"]); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js") && loadedChunks.has("output/8697f_foo_style.module.css_354da7._.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js") && loadedChunks.has("output/8697f_foo_style.module.css_354da7._.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_css_css_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js","output/8697f_foo_style.module.css_354da7._.js","output/8697f_foo_style.css","output/crates_turbopack-tests_tests_snapshot_css_css_input_style.css","output/8697f_foo_style.module_b5a149.css","output/crates_turbopack-tests_tests_snapshot_css_css_input_style.module_b5a149.css"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/index.js (ecmascript)"); } ]); @@ -36,7 +37,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -67,6 +68,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -91,8 +151,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -144,6 +207,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -418,6 +504,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -559,7 +646,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -570,18 +657,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -598,7 +687,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -630,35 +719,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -671,10 +769,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -708,24 +811,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -748,22 +854,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -853,17 +1115,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -966,10 +1246,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -994,18 +1284,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1015,18 +1369,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1047,6 +1440,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1054,7 +1448,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js b/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js new file mode 100644 index 0000000000000..fc083bccefb2a --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js @@ -0,0 +1,1471 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/style.module.css (css, css module)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_export_value__({ + "another-composed-module-style": "another-composed-module-style__style__9bcf751c" + " " + __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)")["foo-module-style"], + "composed-module-style": "composed-module-style__style__9bcf751c" + " " + __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)")["foo-module-style"], + "inner": "inner__style__9bcf751c", + "module-style": "module-style__style__9bcf751c", +}); + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$node_modules$2f$foo$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/style.module.css (css, css module)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +; +; +; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__["default"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$css$2f$css$2f$input$2f$node_modules$2f$foo$2f$style$2e$module$2e$css__$28$css$2c$__css__module$29__["default"]); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/8697f_foo_style.module.css_354da7._.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_css_css_input_index.js_f5704b._.json", ["output/8697f_foo_style.module.css_354da7._.js","output/8697f_foo_style.css","output/crates_turbopack-tests_tests_snapshot_css_css_input_style.css","output/8697f_foo_style.module_b5a149.css","output/crates_turbopack-tests_tests_snapshot_css_css_input_style.module_b5a149.css"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/css/css/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js.map b/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js.map new file mode 100644 index 0000000000000..33116170b9c94 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/css/css/output/crates_turbopack-tests_tests_snapshot_css_css_input_index_c93332.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/[project]/crates/turbopack-tests/tests/snapshot/css/css/input/style.module.css (css, css module)"],"sourcesContent":["__turbopack_export_value__({\n \"another-composed-module-style\": \"another-composed-module-style__style__9bcf751c\" + \" \" + __turbopack_import__(\"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)\")[\"foo-module-style\"],\n \"composed-module-style\": \"composed-module-style__style__9bcf751c\" + \" \" + __turbopack_import__(\"[project]/crates/turbopack-tests/tests/snapshot/css/css/input/node_modules/foo/style.module.css (css, css module)\")[\"foo-module-style\"],\n \"inner\": \"inner__style__9bcf751c\",\n \"module-style\": \"module-style__style__9bcf751c\",\n});\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA"}}, + {"offset": {"line": 10, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 14, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/css/css/input/index.js"],"sourcesContent":["import \"foo/style.css\";\nimport \"foo\";\nimport \"./style.css\";\nimport fooStyle from \"foo/style.module.css\";\nimport style from \"./style.module.css\";\n\nconsole.log(style, fooStyle);\n"],"names":[],"mappings":";;;;;;;;AAMA,QAAQ,GAAG"}}, + {"offset": {"line": 23, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_index.js b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_index.js index bef20d3f18344..8cdc40a1e8274 100644 --- a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_index.js +++ b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/63a02_@emotion_react_index.js", { -"[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { "purposefully empty stub"; "@emtion/react/index.js"; diff --git a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_jsx-dev-runtime.js b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_jsx-dev-runtime.js index 25e470b2c2fca..89fd0a2915b77 100644 --- a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_jsx-dev-runtime.js +++ b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_react_jsx-dev-runtime.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/63a02_@emotion_react_jsx-dev-runtime.js", { -"[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/jsx-dev-runtime.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/jsx-dev-runtime.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { "purposefully empty stub"; "@emtion/react/jsx-dev-runtime.js"; diff --git a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_styled_index.js b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_styled_index.js index c2ec3e7d7308f..f3cef3d4abc40 100644 --- a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_styled_index.js +++ b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/63a02_@emotion_styled_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/63a02_@emotion_styled_index.js", { -"[project]/crates/turbopack-tests/tests/node_modules/@emotion/styled/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/node_modules/@emotion/styled/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { "purposefully empty stub"; "@emtion/styled/index.js"; diff --git a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js new file mode 100644 index 0000000000000..de4295390dbd8 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js @@ -0,0 +1,1476 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/emotion/emotion/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$emotion$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/jsx-dev-runtime.js (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$emotion$2f$react$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/index.js (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$emotion$2f$styled$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/@emotion/styled/index.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +; +; +const StyledButton = __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$emotion$2f$styled$2f$index$2e$js__$28$ecmascript$29__["default"]("button", { + target: "e1r1p2t30", + label: "StyledButton" +})("background:blue;", "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VzIjpbImluZGV4LmpzIl0sInNvdXJjZXNDb250ZW50IjpbIi8qKiBAanN4SW1wb3J0U291cmNlIEBlbW90aW9uL3JlYWN0ICovXG5cbmltcG9ydCB7IGpzeCB9IGZyb20gXCJAZW1vdGlvbi9yZWFjdFwiO1xuaW1wb3J0IHN0eWxlZCBmcm9tIFwiQGVtb3Rpb24vc3R5bGVkXCI7XG5cbmNvbnN0IFN0eWxlZEJ1dHRvbiA9IHN0eWxlZC5idXR0b25gXG4gIGJhY2tncm91bmQ6IGJsdWU7XG5gO1xuXG5mdW5jdGlvbiBDbGFzc05hbWVCdXR0b24oeyBjaGlsZHJlbiB9KSB7XG4gIHJldHVybiAoXG4gICAgPGJ1dHRvblxuICAgICAgY2xhc3NOYW1lPXtjc3NgXG4gICAgICAgIGJhY2tncm91bmQ6IGJsdWU7XG4gICAgICBgfVxuICAgID5cbiAgICAgIHtjaGlsZHJlbn1cbiAgICA8L2J1dHRvbj5cbiAgKTtcbn1cblxuY29uc29sZS5sb2coU3R5bGVkQnV0dG9uLCBDbGFzc05hbWVCdXR0b24pO1xuIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUtxQiJ9 */"); +function ClassNameButton({ children }) { + return __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$emotion$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$28$ecmascript$29__["jsxDEV"]("button", { + className: css` + background: blue; + `, + children: children + }, void 0, false, { + fileName: "", + lineNumber: 12, + columnNumber: 5 + }, this); +} +console.log(StyledButton, ClassNameButton); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_@emotion_react_jsx-dev-runtime.js") && loadedChunks.has("output/63a02_@emotion_react_index.js") && loadedChunks.has("output/63a02_@emotion_styled_index.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index.js_f5704b._.json", ["output/63a02_@emotion_react_jsx-dev-runtime.js","output/63a02_@emotion_react_index.js","output/63a02_@emotion_styled_index.js"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/emotion/emotion/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js.map b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js.map new file mode 100644 index 0000000000000..a098fa9af6b76 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/emotion/emotion/input/index.js"],"sourcesContent":["/** @jsxImportSource @emotion/react */\n\nimport { jsx } from \"@emotion/react\";\nimport styled from \"@emotion/styled\";\n\nconst StyledButton = styled.button`\n background: blue;\n`;\n\nfunction ClassNameButton({ children }) {\n return (\n \n {children}\n \n );\n}\n\nconsole.log(StyledButton, ClassNameButton);\n"],"names":[],"mappings":";;;;;;;AAKA,MAAM;;;;AAIN,SAAS,gBAAgB,EAAE,SAAQ,EAAE,EAAE;IACrC,OACE,0LAAC;QACC,WAAW,GAAG,CAAC;;MAEf,CAAC;kBAEA;;;;;;AAGP;AAEA,QAAQ,GAAG,CAAC,cAAc"}}, + {"offset": {"line": 28, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_d9fc1b.js b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_d9fc1b.js index 0e0b94d8b0b40..e14fe60721bea 100644 --- a/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_d9fc1b.js +++ b/crates/turbopack-tests/tests/snapshot/emotion/emotion/output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_d9fc1b.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_d9fc1b.js", { -"[project]/crates/turbopack-tests/tests/snapshot/emotion/emotion/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/emotion/emotion/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$emotion$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/jsx-dev-runtime.js (ecmascript)"); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$emotion$2f$react$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/@emotion/react/index.js (ecmascript)"); @@ -28,8 +28,9 @@ function ClassNameButton({ children }) { console.log(StyledButton, ClassNameButton); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/63a02_@emotion_react_jsx-dev-runtime.js") && loadedChunks.has("output/63a02_@emotion_react_index.js") && loadedChunks.has("output/63a02_@emotion_styled_index.js") && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_@emotion_react_jsx-dev-runtime.js") && loadedChunks.has("output/63a02_@emotion_react_index.js") && loadedChunks.has("output/63a02_@emotion_styled_index.js") && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index.js_f5704b._.json", ["output/63a02_@emotion_react_jsx-dev-runtime.js","output/63a02_@emotion_react_index.js","output/63a02_@emotion_styled_index.js","output/crates_turbopack-tests_tests_snapshot_emotion_emotion_input_index_549658.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/emotion/emotion/input/index.js (ecmascript)"); } ]); @@ -41,7 +42,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -72,6 +73,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -96,8 +156,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -149,6 +212,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -423,6 +509,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -564,7 +651,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -575,18 +662,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -603,7 +692,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -635,35 +724,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -676,10 +774,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -713,24 +816,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -753,22 +859,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -858,17 +1120,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -971,10 +1251,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -999,18 +1289,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1020,18 +1374,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1052,6 +1445,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1059,7 +1453,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_93aa94._.js b/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_93aa94._.js index 0e5f3d4594542..12a7d91bc63cb 100644 --- a/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_93aa94._.js +++ b/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_93aa94._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_env_env_input_93aa94._.js", { -"[project]/crates/turbopack-tests/tests/snapshot/env/env/input/.env/.env.js": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/env/env/input/.env/.env.js": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { const env = process.env = {...process.env}; @@ -8,15 +8,16 @@ env["FOO"] = foo; env["FOOBAR"] = foobar; })()), -"[project]/crates/turbopack-tests/tests/snapshot/env/env/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/env/env/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { console.log(process.env.FOOBAR); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_env_env_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/env/env/input/.env/.env.js"); -instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/env/env/input/index.js (ecmascript)"); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/env/env/input/index.js (ecmascript)"); } ]); (() => { @@ -27,7 +28,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -58,6 +59,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -82,8 +142,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -135,6 +198,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -409,6 +495,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -550,7 +637,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -561,18 +648,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -589,7 +678,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -621,35 +710,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -662,10 +760,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -699,24 +802,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -739,22 +845,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -844,17 +1106,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -957,10 +1237,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -985,18 +1275,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1006,18 +1360,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1038,6 +1431,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1045,7 +1439,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js b/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js new file mode 100644 index 0000000000000..705d5236fb2de --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js @@ -0,0 +1,1462 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/env/env/input/.env/.env.js": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +const env = process.env = {...process.env}; + +env["FOO"] = foo; +env["FOOBAR"] = foobar; + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/env/env/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +console.log(process.env.FOOBAR); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_env_env_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/env/env/input/.env/.env.js"); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/env/env/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js.map b/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js.map new file mode 100644 index 0000000000000..ecb3f5cd6c158 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/env/env/output/crates_turbopack-tests_tests_snapshot_env_env_input_bb8ee7._.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 12, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/env/env/input/index.js"],"sourcesContent":["console.log(process.env.FOOBAR);\n"],"names":[],"mappings":"AAAA,QAAQ,GAAG,CAAC,QAAQ,GAAG,CAAC,MAAM"}}, + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_09dc6c.js b/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_09dc6c.js index 97c189e2dd069..f164e68f00b3a 100644 --- a/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_09dc6c.js +++ b/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_09dc6c.js @@ -1,12 +1,13 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_09dc6c.js", { -"[project]/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { console.log("hello world"); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js"))) return true; + registerChunkList("output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index.js_f5704b._.json", ["output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/input/index.js (ecmascript)"); } ]); @@ -18,7 +19,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -49,6 +50,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -73,8 +133,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -126,6 +189,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -400,6 +486,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -541,7 +628,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -552,18 +639,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -580,7 +669,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -612,35 +701,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -653,10 +751,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -690,24 +793,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -730,22 +836,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -835,17 +1097,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -948,10 +1228,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -976,18 +1266,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -997,18 +1351,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1029,6 +1422,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1036,7 +1430,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js b/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js new file mode 100644 index 0000000000000..5d08d899032d8 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js @@ -0,0 +1,1453 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +console.log("hello world"); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js.map b/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js.map new file mode 100644 index 0000000000000..2cc43c7bce8c6 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/output/a587c_tests_snapshot_evaluated_entrry_runtime_entry_input_index_19edf2.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/evaluated_entrry/runtime_entry/input/index.js"],"sourcesContent":["console.log(\"hello world\");\n"],"names":[],"mappings":"AAAA,QAAQ,GAAG,CAAC"}}, + {"offset": {"line": 5, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js b/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js new file mode 100644 index 0000000000000..f455e8348de0c --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js @@ -0,0 +1,1453 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/example/example/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +console.log("hello world"); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_example_example_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/example/example/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js.map b/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js.map new file mode 100644 index 0000000000000..0a1d13ff0cbc9 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/example/example/input/index.js"],"sourcesContent":["console.log(\"hello world\");\n"],"names":[],"mappings":"AAAA,QAAQ,GAAG,CAAC"}}, + {"offset": {"line": 5, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_29e0b9.js b/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_29e0b9.js index 3143934f40067..24df4ed2ffeae 100644 --- a/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_29e0b9.js +++ b/crates/turbopack-tests/tests/snapshot/example/example/output/crates_turbopack-tests_tests_snapshot_example_example_input_index_29e0b9.js @@ -1,12 +1,13 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_example_example_input_index_29e0b9.js", { -"[project]/crates/turbopack-tests/tests/snapshot/example/example/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/example/example/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { console.log("hello world"); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_example_example_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_example_example_input_index_0a08ce.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/example/example/input/index.js (ecmascript)"); } ]); @@ -18,7 +19,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -49,6 +50,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -73,8 +133,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -126,6 +189,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -400,6 +486,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -541,7 +628,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -552,18 +639,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -580,7 +669,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -612,35 +701,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -653,10 +751,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -690,24 +793,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -730,22 +836,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -835,17 +1097,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -948,10 +1228,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -976,18 +1266,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -997,18 +1351,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1029,6 +1422,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1036,7 +1430,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js new file mode 100644 index 0000000000000..7f2e473ad04b4 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js @@ -0,0 +1,1479 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/commonjs.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +exports.hello = "World"; + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/c.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_esm__({}); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$commonjs$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/commonjs.js (ecmascript)"); +__turbopack_cjs__(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$commonjs$2e$js__$28$ecmascript$29__); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/b.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_esm__({}); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$c$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/c.js (ecmascript)"); +__turbopack_cjs__(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$c$2e$js__$28$ecmascript$29__); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$b$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/b.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$b$2e$js__$28$ecmascript$29__); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js.map b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js.map new file mode 100644 index 0000000000000..424ae172c321a --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js.map @@ -0,0 +1,12 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/commonjs.js"],"sourcesContent":["// commonjs.js\nexports.hello = \"World\";\n\n"],"names":[],"mappings":"AACA,QAAQ,KAAK,GAAG"}}, + {"offset": {"line": 5, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 9, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 14, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 23, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 27, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/index.js"],"sourcesContent":["// a.js\nimport * as B from \"./b\";\nconsole.log(B);"],"names":[],"mappings":";;;AAEA,QAAQ,GAAG"}}, + {"offset": {"line": 31, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d9c332.js b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d9c332.js index 10759444a5734..80ebccc733b25 100644 --- a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d9c332.js +++ b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d9c332.js @@ -1,11 +1,11 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d9c332.js", { -"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/commonjs.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/commonjs.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { exports.hello = "World"; }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/c.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/c.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({}); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$commonjs$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/commonjs.js (ecmascript)"); @@ -14,7 +14,7 @@ __turbopack_cjs__(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turb ; })()), -"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/b.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/b.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({}); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$c$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/c.js (ecmascript)"); @@ -23,7 +23,7 @@ __turbopack_cjs__(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turb ; })()), -"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$b$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/b.js (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -31,8 +31,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$2$2f$input$2f$b$2e$js__$28$ecmascript$29__); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-2_input_index_d5f22a.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-2/input/index.js (ecmascript)"); } ]); @@ -44,7 +45,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -75,6 +76,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -99,8 +159,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -152,6 +215,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -426,6 +512,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -567,7 +654,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -578,18 +665,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -606,7 +695,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -638,35 +727,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -679,10 +777,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -716,24 +819,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -756,22 +862,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -861,17 +1123,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -974,10 +1254,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -1002,18 +1292,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1023,18 +1377,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1055,6 +1448,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1062,7 +1456,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_b23628.js b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_b23628.js index fe91a986eabcf..c6a9c61c81555 100644 --- a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_b23628.js +++ b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_b23628.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_b23628.js", { -"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/exported.cjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/exported.cjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { module.exports = { foo: 1, @@ -8,7 +8,7 @@ module.exports = { }; }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/mod.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/mod.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({}); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$script$2f$input$2f$exported$2e$cjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/exported.cjs (ecmascript)"); @@ -18,7 +18,7 @@ __turbopack_cjs__(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turb console.log('Hoist test'); })()), -"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$script$2f$input$2f$mod$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/mod.js (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -26,8 +26,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$script$2f$input$2f$mod$2e$js__$28$ecmascript$29__); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/index.js (ecmascript)"); } ]); @@ -39,7 +40,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -70,6 +71,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -94,8 +154,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -147,6 +210,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -421,6 +507,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -562,7 +649,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -573,18 +660,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -601,7 +690,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -633,35 +722,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -674,10 +772,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -711,24 +814,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -751,22 +857,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -856,17 +1118,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -969,10 +1249,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -997,18 +1287,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1018,18 +1372,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1050,6 +1443,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1057,7 +1451,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js new file mode 100644 index 0000000000000..427ad1af3099c --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js @@ -0,0 +1,1474 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/exported.cjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +module.exports = { + foo: 1, + bar: 2 +}; + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/mod.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_esm__({}); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$script$2f$input$2f$exported$2e$cjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/exported.cjs (ecmascript)"); +__turbopack_cjs__(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$script$2f$input$2f$exported$2e$cjs__$28$ecmascript$29__); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +console.log('Hoist test'); + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$script$2f$input$2f$mod$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/mod.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$export$2d$alls$2f$cjs$2d$script$2f$input$2f$mod$2e$js__$28$ecmascript$29__); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js.map b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js.map new file mode 100644 index 0000000000000..9153a98a4b25d --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/output/crates_turbopack-tests_tests_snapshot_export-alls_cjs-script_input_index_ed5b85.js.map @@ -0,0 +1,10 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/exported.cjs"],"sourcesContent":["module.exports = { foo: 1, bar: 2 }"],"names":[],"mappings":"AAAA,OAAO,OAAO,GAAG;IAAE,KAAK;IAAG,KAAK;AAAE"}}, + {"offset": {"line": 8, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 12, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/mod.js"],"sourcesContent":["\nexport * from './exported.cjs'\n\nconsole.log('Hoist test')"],"names":[],"mappings":";;;;;AAGA,QAAQ,GAAG,CAAC"}}, + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 22, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/export-alls/cjs-script/input/index.js"],"sourcesContent":["import * as foo from './mod.js';\n\nconsole.log(foo)"],"names":[],"mappings":";;;AAEA,QAAQ,GAAG"}}, + {"offset": {"line": 26, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_3e3da0.js b/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_3e3da0.js index 6773850011b4e..feb312b7a39ea 100644 --- a/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_3e3da0.js +++ b/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_3e3da0.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_3e3da0.js", { -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { const __TURBOPACK__import$2e$meta__ = { url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs" @@ -9,15 +9,16 @@ const __TURBOPACK__import$2e$meta__ = { console.log(__TURBOPACK__import$2e$meta__.url); }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$cjs$2f$input$2f$mod$2e$cjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; ; }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/index.js (ecmascript)"); } ]); @@ -29,7 +30,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -60,6 +61,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -84,8 +144,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -137,6 +200,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -411,6 +497,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -552,7 +639,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -563,18 +650,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -591,7 +680,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -623,35 +712,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -664,10 +762,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -701,24 +804,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -741,22 +847,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -846,17 +1108,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -959,10 +1239,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -987,18 +1277,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1008,18 +1362,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1040,6 +1433,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1047,7 +1441,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js b/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js new file mode 100644 index 0000000000000..4b26fe5e75923 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js @@ -0,0 +1,1464 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +const __TURBOPACK__import$2e$meta__ = { + url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs" +}; +"__TURBOPACK__ecmascript__hoisting__location__"; +console.log(__TURBOPACK__import$2e$meta__.url); + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$cjs$2f$input$2f$mod$2e$cjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js.map b/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js.map new file mode 100644 index 0000000000000..7a578bd5ccc12 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/cjs/output/crates_turbopack-tests_tests_snapshot_import-meta_cjs_input_index_746b39.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/import-meta/cjs/input/mod.cjs"],"sourcesContent":["console.log(import.meta.url);\n"],"names":[],"mappings":";;;;AAAA,QAAQ,GAAG,CAAC,8BAAY,GAAG"}}, + {"offset": {"line": 9, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 16, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_090618.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_090618.js index 2f214f72134ed..3fa1e091e0624 100644 --- a/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_090618.js +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_090618.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_090618.js", { -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { const __TURBOPACK__import$2e$meta__ = { url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs" @@ -16,15 +16,16 @@ foo(); bar(); }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2d$multiple$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; ; }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index.js_f5704b._.json", ["output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/index.js (ecmascript)"); } ]); @@ -36,7 +37,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -67,6 +68,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -91,8 +151,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -144,6 +207,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -418,6 +504,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -559,7 +646,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -570,18 +657,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -598,7 +687,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -630,35 +719,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -671,10 +769,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -708,24 +811,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -748,22 +854,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -853,17 +1115,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -966,10 +1246,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -994,18 +1284,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1015,18 +1369,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1047,6 +1440,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1054,7 +1448,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js new file mode 100644 index 0000000000000..66c8598f008a0 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js @@ -0,0 +1,1471 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +const __TURBOPACK__import$2e$meta__ = { + url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs" +}; +"__TURBOPACK__ecmascript__hoisting__location__"; +function foo() { + console.log(__TURBOPACK__import$2e$meta__.url); +} +function bar() { + console.log(__TURBOPACK__import$2e$meta__.url); +} +foo(); +bar(); + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2d$multiple$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js.map b/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js.map new file mode 100644 index 0000000000000..9716b8f531c0e --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-multiple_input_index_8687d1.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/import-meta/esm-multiple/input/mod.mjs"],"sourcesContent":["function foo() {\n console.log(import.meta.url);\n}\nfunction bar() {\n console.log(import.meta.url);\n}\n\nfoo();\nbar();\n"],"names":[],"mappings":";;;;AAAA,SAAS,MAAM;IACb,QAAQ,GAAG,CAAC,8BAAY,GAAG;AAC7B;AACA,SAAS,MAAM;IACb,QAAQ,GAAG,CAAC,8BAAY,GAAG;AAC7B;AAEA;AACA"}}, + {"offset": {"line": 16, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 20, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 23, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js new file mode 100644 index 0000000000000..36b2b517b8d5d --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js @@ -0,0 +1,1464 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +const __TURBOPACK__import$2e$meta__ = { + url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs" +}; +"__TURBOPACK__ecmascript__hoisting__location__"; +__TURBOPACK__import$2e$meta__.foo = 1; + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2d$mutable$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js.map b/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js.map new file mode 100644 index 0000000000000..e954bf2efdd13 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs"],"sourcesContent":["import.meta.foo = 1;\n"],"names":[],"mappings":";;;;AAAA,8BAAY,GAAG,GAAG"}}, + {"offset": {"line": 9, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 16, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_dc37f8.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_dc37f8.js index cbe976b62c871..a7ba9eda1bf26 100644 --- a/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_dc37f8.js +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_dc37f8.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_dc37f8.js", { -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { const __TURBOPACK__import$2e$meta__ = { url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs" @@ -9,15 +9,16 @@ const __TURBOPACK__import$2e$meta__ = { __TURBOPACK__import$2e$meta__.foo = 1; }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2d$mutable$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/mod.mjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; ; }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_import-meta_esm-mutable_input_index_3487d2.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-mutable/input/index.js (ecmascript)"); } ]); @@ -29,7 +30,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -60,6 +61,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -84,8 +144,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -137,6 +200,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -411,6 +497,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -552,7 +639,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -563,18 +650,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -591,7 +680,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -623,35 +712,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -664,10 +762,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -701,24 +804,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -741,22 +847,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -846,17 +1108,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -959,10 +1239,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -987,18 +1277,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1008,18 +1362,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1040,6 +1433,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1047,7 +1441,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_040a52.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_040a52.js index 96518a6b9a4b1..2edbd0f88b040 100644 --- a/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_040a52.js +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_040a52.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_040a52.js", { -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { const __TURBOPACK__import$2e$meta__ = { url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs" @@ -9,15 +9,16 @@ const __TURBOPACK__import$2e$meta__ = { console.log(__TURBOPACK__import$2e$meta__); }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2d$object$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; ; }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/index.js (ecmascript)"); } ]); @@ -29,7 +30,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -60,6 +61,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -84,8 +144,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -137,6 +200,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -411,6 +497,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -552,7 +639,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -563,18 +650,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -591,7 +680,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -623,35 +712,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -664,10 +762,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -701,24 +804,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -741,22 +847,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -846,17 +1108,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -959,10 +1239,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -987,18 +1277,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1008,18 +1362,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1040,6 +1433,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1047,7 +1441,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js new file mode 100644 index 0000000000000..efd0eaed44f06 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js @@ -0,0 +1,1464 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +const __TURBOPACK__import$2e$meta__ = { + url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs" +}; +"__TURBOPACK__ecmascript__hoisting__location__"; +console.log(__TURBOPACK__import$2e$meta__); + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2d$object$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js.map b/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js.map new file mode 100644 index 0000000000000..87f763dce3d88 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/output/crates_turbopack-tests_tests_snapshot_import-meta_esm-object_input_index_73c2df.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/import-meta/esm-object/input/mod.mjs"],"sourcesContent":["console.log(import.meta);\n"],"names":[],"mappings":";;;;AAAA,QAAQ,GAAG"}}, + {"offset": {"line": 9, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 16, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_c633a8.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_c633a8.js index ddc4030a65b39..eccf4fc573d09 100644 --- a/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_c633a8.js +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_c633a8.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_c633a8.js", { -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { const __TURBOPACK__import$2e$meta__ = { url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs" @@ -9,15 +9,16 @@ const __TURBOPACK__import$2e$meta__ = { console.log(__TURBOPACK__import$2e$meta__.url); }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; ; }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/index.js (ecmascript)"); } ]); @@ -29,7 +30,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -60,6 +61,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -84,8 +144,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -137,6 +200,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -411,6 +497,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -552,7 +639,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -563,18 +650,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -591,7 +680,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -623,35 +712,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -664,10 +762,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -701,24 +804,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -741,22 +847,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -846,17 +1108,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -959,10 +1239,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -987,18 +1277,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1008,18 +1362,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1040,6 +1433,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1047,7 +1441,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js b/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js new file mode 100644 index 0000000000000..e1cbbc1ecc5de --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js @@ -0,0 +1,1464 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +const __TURBOPACK__import$2e$meta__ = { + url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs" +}; +"__TURBOPACK__ecmascript__hoisting__location__"; +console.log(__TURBOPACK__import$2e$meta__.url); + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$esm$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js.map b/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js.map new file mode 100644 index 0000000000000..000fb952f2abc --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/esm/output/crates_turbopack-tests_tests_snapshot_import-meta_esm_input_index_fe8e61.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/import-meta/esm/input/mod.mjs"],"sourcesContent":["console.log(import.meta.url);\n"],"names":[],"mappings":";;;;AAAA,QAAQ,GAAG,CAAC,8BAAY,GAAG"}}, + {"offset": {"line": 9, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 16, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js b/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js new file mode 100644 index 0000000000000..1ef6e29879a30 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js @@ -0,0 +1,1470 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/asset.txt (static)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_export_value__("/crates/turbopack-tests/tests/snapshot/import-meta/url/static/05254cf29a922ae2.txt"); +})()), +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +const __TURBOPACK__import$2e$meta__ = { + url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs" +}; +"__TURBOPACK__ecmascript__hoisting__location__"; +const assetUrl = new URL(__turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/asset.txt (static)"), location.origin); +console.log(assetUrl); +fetch(assetUrl).then((res)=>res.text()).then(console.log); + +}.call(this) }), +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$url$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js.map b/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js.map new file mode 100644 index 0000000000000..1628a96f8f796 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 8, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs"],"sourcesContent":["const assetUrl = new URL('./asset.txt', import.meta.url);\n\nconsole.log(assetUrl);\nfetch(assetUrl)\n .then(res => res.text())\n .then(console.log);\n"],"names":[],"mappings":";;;;AAAA,MAAM,WAAW,IAAI;AAErB,QAAQ,GAAG,CAAC;AACZ,MAAM,UACH,IAAI,CAAC,CAAA,MAAO,IAAI,IAAI,IACpB,IAAI,CAAC,QAAQ,GAAG"}}, + {"offset": {"line": 15, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 19, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 22, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_f01bae.js b/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_f01bae.js index 4eda5243bff72..17a3d628c9ef7 100644 --- a/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_f01bae.js +++ b/crates/turbopack-tests/tests/snapshot/import-meta/url/output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_f01bae.js @@ -1,10 +1,10 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_f01bae.js", { -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/asset.txt (static)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/asset.txt (static)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__("/crates/turbopack-tests/tests/snapshot/import-meta/url/static/05254cf29a922ae2.txt"); })()), -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { const __TURBOPACK__import$2e$meta__ = { url: "file:///ROOT/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs" @@ -15,15 +15,16 @@ console.log(assetUrl); fetch(assetUrl).then((res)=>res.text()).then(console.log); }.call(this) }), -"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$import$2d$meta$2f$url$2f$input$2f$mod$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/mod.mjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; ; }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_import-meta_url_input_index_5f69bf.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/import-meta/url/input/index.js (ecmascript)"); } ]); @@ -35,7 +36,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -66,6 +67,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -90,8 +150,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -143,6 +206,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -417,6 +503,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -558,7 +645,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -569,18 +656,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -597,7 +686,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -629,35 +718,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -670,10 +768,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -707,24 +810,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -747,22 +853,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -852,17 +1114,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -965,10 +1245,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -993,18 +1283,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1014,18 +1368,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1046,6 +1439,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1053,7 +1447,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js new file mode 100644 index 0000000000000..64bd0e4b685ea --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js @@ -0,0 +1,1467 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_export_value__((__turbopack_import__) => { + return __turbopack_load__("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_93c5e8._.js").then(() => { + return __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk)"); + }).then((chunks_paths) => { + __turbopack_register_chunk_list__("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_d744dd._.json", chunks_paths); + return Promise.all(chunks_paths.map((chunk_path) => __turbopack_load__(chunk_path))); + }).then(() => { + return __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript)"); + }); +}); + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +__turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)")(__turbopack_import__).then(console.log); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js.map b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js.map new file mode 100644 index 0000000000000..4de4d630c6205 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js"],"sourcesContent":["import(\"./vercel.mjs\").then(console.log);\n"],"names":[],"mappings":"AAAA,qKAAuB,IAAI,CAAC,QAAQ,GAAG"}}, + {"offset": {"line": 19, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js index 55779f90c37ba..6780519082913 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js +++ b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js @@ -1,11 +1,12 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__((__turbopack_import__) => { return __turbopack_load__("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_93c5e8._.js").then(() => { return __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk)"); }).then((chunks_paths) => { + __turbopack_register_chunk_list__("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_d744dd._.json", chunks_paths); return Promise.all(chunks_paths.map((chunk_path) => __turbopack_load__(chunk_path))); }).then(() => { return __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript)"); @@ -13,13 +14,14 @@ __turbopack_export_value__((__turbopack_import__) => { }); })()), -"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)")(__turbopack_import__).then(console.log); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_56419a.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js (ecmascript)"); } ]); @@ -31,7 +33,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -62,6 +64,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -86,8 +147,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -139,6 +203,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -413,6 +500,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -554,7 +642,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -565,18 +653,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -593,7 +683,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -625,35 +715,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -666,10 +765,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -703,24 +807,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -743,22 +850,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -848,17 +1111,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -961,10 +1242,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -989,18 +1280,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1010,18 +1365,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1042,6 +1436,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1049,7 +1444,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js.map b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js.map index 7e424ee4e5d5f..4de4d630c6205 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js.map +++ b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_index_6f9eb3.js.map @@ -1,6 +1,6 @@ { "version": 3, "sections": [ - {"offset": {"line": 17, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js"],"sourcesContent":["import(\"./vercel.mjs\").then(console.log);\n"],"names":[],"mappings":"AAAA,qKAAuB,IAAI,CAAC,QAAQ,GAAG"}}, - {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/index.js"],"sourcesContent":["import(\"./vercel.mjs\").then(console.log);\n"],"names":[],"mappings":"AAAA,qKAAuB,IAAI,CAAC,QAAQ,GAAG"}}, + {"offset": {"line": 19, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] } \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs._.js b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs._.js index 1357b30c228ca..50ffebf121736 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs._.js +++ b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs._.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "default": ()=>__TURBOPACK__default__export__ diff --git a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_93c5e8._.js b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_93c5e8._.js index 2de3cd40902ce..3d07e709d38cc 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_93c5e8._.js +++ b/crates/turbopack-tests/tests/snapshot/imports/dynamic/output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_93c5e8._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs_93c5e8._.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/dynamic/input/vercel.mjs (ecmascript, manifest chunk)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__([ "output/crates_turbopack-tests_tests_snapshot_imports_dynamic_input_vercel.mjs._.js" diff --git a/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js b/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js new file mode 100644 index 0000000000000..9854d9a96d405 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js @@ -0,0 +1,1468 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json (json)": (() => {{ + +throw new Error("An error occurred while generating the chunk item [project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json (json)\n at Execution of module_factory failed\n at Execution of JsonChunkItem::content failed\n at Unable to make a module from invalid JSON: expected `,` or `}` at line 3 column 26\n at nested.?\n 1 | {\n 2 | \"nested\": {\n | v\n 3 + \"this-is\": \"invalid\" // lint-staged will remove trailing commas, so here's a comment\n | ^\n 4 | }\n 5 | }\n"); + +}}), +"[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/package.json (json)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_export_value__(JSON.parse("{\"name\":\"json-snapshot\"}")); +})()), +"[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$json$2f$input$2f$package$2e$json__$28$json$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/package.json (json)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$json$2f$input$2f$invalid$2e$json__$28$json$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json (json)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$json$2f$input$2f$package$2e$json__$28$json$29__["default"].name); +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$json$2f$input$2f$invalid$2e$json__$28$json$29__["default"]["this-is"]); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_imports_json_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js.map b/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js.map new file mode 100644 index 0000000000000..483ffc655fc99 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/json/input/index.js"],"sourcesContent":["import pkg from \"./package.json\";\nconsole.log(pkg.name);\nimport invalid from \"./invalid.json\";\nconsole.log(invalid[\"this-is\"]);\n"],"names":[],"mappings":";;;;AACA,QAAQ,GAAG,CAAC,2KAAI,IAAI;;AAEpB,QAAQ,GAAG,CAAC,0KAAO,CAAC,UAAU"}}, + {"offset": {"line": 20, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_e460e9.js b/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_e460e9.js index 94f923bcc7209..5a953d3117a30 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_e460e9.js +++ b/crates/turbopack-tests/tests/snapshot/imports/json/output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_e460e9.js @@ -5,11 +5,11 @@ throw new Error("An error occurred while generating the chunk item [project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json (json)\n at Execution of module_factory failed\n at Execution of JsonChunkItem::content failed\n at Unable to make a module from invalid JSON: expected `,` or `}` at line 3 column 26\n at nested.?\n 1 | {\n 2 | \"nested\": {\n | v\n 3 + \"this-is\": \"invalid\" // lint-staged will remove trailing commas, so here's a comment\n | ^\n 4 | }\n 5 | }\n"); }}), -"[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/package.json (json)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/package.json (json)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__(JSON.parse("{\"name\":\"json-snapshot\"}")); })()), -"[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$json$2f$input$2f$package$2e$json__$28$json$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/package.json (json)"); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$json$2f$input$2f$invalid$2e$json__$28$json$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/invalid.json (json)"); @@ -20,8 +20,9 @@ console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$ console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$json$2f$input$2f$invalid$2e$json__$28$json$29__["default"]["this-is"]); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_imports_json_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_imports_json_input_index_881b1b.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/json/input/index.js (ecmascript)"); } ]); @@ -33,7 +34,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -64,6 +65,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -88,8 +148,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -141,6 +204,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -415,6 +501,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -556,7 +643,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -567,18 +654,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -595,7 +684,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -627,35 +716,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -668,10 +766,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -705,24 +808,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -745,22 +851,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -850,17 +1112,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -963,10 +1243,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -991,18 +1281,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1012,18 +1366,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1044,6 +1437,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1051,7 +1445,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js new file mode 100644 index 0000000000000..998ffda23838d --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js @@ -0,0 +1,1458 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { + +const dne = __turbopack_require__((()=>{ + const e = new Error("Cannot find module 'does-not-exist/path'"); + e.code = 'MODULE_NOT_FOUND'; + throw e; +})()); +console.log(dne); + +}.call(this) }), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js.map b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js.map new file mode 100644 index 0000000000000..0d6ececf1247c --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/input/index.js"],"sourcesContent":["const dne = require(\"does-not-exist/path\");\n\nconsole.log(dne);\n"],"names":[],"mappings":"AAAA,MAAM,MAAM;;;;;AAEZ,QAAQ,GAAG,CAAC"}}, + {"offset": {"line": 10, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_fb56eb.js b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_fb56eb.js index 3da4a929bc237..751eb513a337a 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_fb56eb.js +++ b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_fb56eb.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_fb56eb.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/input/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { const dne = __turbopack_require__((()=>{ const e = new Error("Cannot find module 'does-not-exist/path'"); @@ -10,8 +10,9 @@ const dne = __turbopack_require__((()=>{ console.log(dne); }.call(this) }), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index.js_f5704b._.json", ["output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_cjs_input_index_707ee1.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_cjs/input/index.js (ecmascript)"); } ]); @@ -23,7 +24,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -54,6 +55,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -78,8 +138,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -131,6 +194,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -405,6 +491,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -546,7 +633,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -557,18 +644,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -585,7 +674,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -617,35 +706,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -658,10 +756,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -695,24 +798,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -735,22 +841,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -840,17 +1102,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -953,10 +1233,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -981,18 +1271,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1002,18 +1356,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1034,6 +1427,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1041,7 +1435,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js new file mode 100644 index 0000000000000..fbd972f8eaf33 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js @@ -0,0 +1,1461 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +(()=>{ + const e = new Error("Cannot find module 'does-not-exist/path'"); + e.code = 'MODULE_NOT_FOUND'; + throw e; +})(); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +console.log(dne); +console.log({}[dne]); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js.map b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js.map new file mode 100644 index 0000000000000..d13204ccb8a96 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/input/index.js"],"sourcesContent":["import dne from \"does-not-exist/path\";\n\nconsole.log(dne);\nconsole.log({}[dne]);\n"],"names":[],"mappings":";;;;;;;AAEA,QAAQ,GAAG,CAAC;AACZ,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI"}}, + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_ee6078.js b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_ee6078.js index 7b73d92eccbb9..0bd9d2b984d05 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_ee6078.js +++ b/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_ee6078.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_ee6078.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { (()=>{ const e = new Error("Cannot find module 'does-not-exist/path'"); @@ -13,8 +13,9 @@ console.log(dne); console.log({}[dne]); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index.js_f5704b._.json", ["output/79fb1_turbopack-tests_tests_snapshot_imports_resolve_error_esm_input_index_af6491.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/resolve_error_esm/input/index.js (ecmascript)"); } ]); @@ -26,7 +27,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -57,6 +58,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -81,8 +141,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -134,6 +197,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -408,6 +494,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -549,7 +636,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -560,18 +647,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -588,7 +677,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -620,35 +709,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -661,10 +759,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -698,24 +801,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -738,22 +844,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -843,17 +1105,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -956,10 +1236,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -984,18 +1274,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1005,18 +1359,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1037,6 +1430,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1044,7 +1438,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js new file mode 100644 index 0000000000000..50974ed6615cc --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js @@ -0,0 +1,1479 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_export_value__((__turbopack_import__) => { + return __turbopack_load__("output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_93c5e8._.js").then(() => { + return __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk)"); + }).then((chunks_paths) => { + __turbopack_register_chunk_list__("output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_d744dd._.json", chunks_paths); + return Promise.all(chunks_paths.map((chunk_path) => __turbopack_load__(chunk_path))); + }).then(() => { + return __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)"); + }); +}); + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_esm__({ + "default": ()=>__TURBOPACK__default__export__ +}); +const __TURBOPACK__default__export__ = "turbopack"; + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$static$2d$and$2d$dynamic$2f$input$2f$vercel$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$static$2d$and$2d$dynamic$2f$input$2f$vercel$2e$mjs__$28$ecmascript$29__["default"]); +__turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)")(__turbopack_import__).then(console.log); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js.map b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js.map new file mode 100644 index 0000000000000..db5d1dd46e145 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs"],"sourcesContent":["export default \"turbopack\";\n"],"names":[],"mappings":";;;uCAAe"}}, + {"offset": {"line": 22, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 26, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js"],"sourcesContent":["import img from \"./vercel.mjs\";\nconsole.log(img);\n\nimport(\"./vercel.mjs\").then(console.log);\n"],"names":[],"mappings":";;;AACA,QAAQ,GAAG;AAEX,gLAAuB,IAAI,CAAC,QAAQ,GAAG"}}, + {"offset": {"line": 31, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js index efbdcff5c977f..d535883079762 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js +++ b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js @@ -1,11 +1,12 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__((__turbopack_import__) => { return __turbopack_load__("output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_93c5e8._.js").then(() => { return __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk)"); }).then((chunks_paths) => { + __turbopack_register_chunk_list__("output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_d744dd._.json", chunks_paths); return Promise.all(chunks_paths.map((chunk_path) => __turbopack_load__(chunk_path))); }).then(() => { return __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)"); @@ -13,7 +14,7 @@ __turbopack_export_value__((__turbopack_import__) => { }); })()), -"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "default": ()=>__TURBOPACK__default__export__ @@ -21,7 +22,7 @@ __turbopack_esm__({ const __TURBOPACK__default__export__ = "turbopack"; })()), -"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$static$2d$and$2d$dynamic$2f$input$2f$vercel$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -30,8 +31,9 @@ console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$ __turbopack_require__("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk, loader)")(__turbopack_import__).then(console.log); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index.js_f5704b._.json", ["output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_507785.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js (ecmascript)"); } ]); @@ -43,7 +45,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -74,6 +76,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -98,8 +159,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -151,6 +215,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -425,6 +512,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -566,7 +654,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -577,18 +665,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -605,7 +695,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -637,35 +727,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -678,10 +777,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -715,24 +819,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -755,22 +862,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -860,17 +1123,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -973,10 +1254,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -1001,18 +1292,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1022,18 +1377,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1054,6 +1448,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1061,7 +1456,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js.map b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js.map index 275483d690c9e..db5d1dd46e145 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js.map +++ b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_index_899ad5.js.map @@ -1,8 +1,8 @@ { "version": 3, "sections": [ - {"offset": {"line": 17, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs"],"sourcesContent":["export default \"turbopack\";\n"],"names":[],"mappings":";;;uCAAe"}}, - {"offset": {"line": 21, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, - {"offset": {"line": 25, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js"],"sourcesContent":["import img from \"./vercel.mjs\";\nconsole.log(img);\n\nimport(\"./vercel.mjs\").then(console.log);\n"],"names":[],"mappings":";;;AACA,QAAQ,GAAG;AAEX,gLAAuB,IAAI,CAAC,QAAQ,GAAG"}}, - {"offset": {"line": 30, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] + {"offset": {"line": 18, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs"],"sourcesContent":["export default \"turbopack\";\n"],"names":[],"mappings":";;;uCAAe"}}, + {"offset": {"line": 22, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 26, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/index.js"],"sourcesContent":["import img from \"./vercel.mjs\";\nconsole.log(img);\n\nimport(\"./vercel.mjs\").then(console.log);\n"],"names":[],"mappings":";;;AACA,QAAQ,GAAG;AAEX,gLAAuB,IAAI,CAAC,QAAQ,GAAG"}}, + {"offset": {"line": 31, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] } \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs._.js b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs._.js index ce81794e7177f..7fff51cea0f28 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs._.js +++ b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs._.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "default": ()=>__TURBOPACK__default__export__ diff --git a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_93c5e8._.js b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_93c5e8._.js index c9f4caba61a8c..b78eb7686bf45 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_93c5e8._.js +++ b/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_93c5e8._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs_93c5e8._.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/static-and-dynamic/input/vercel.mjs (ecmascript, manifest chunk)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__([ "output/79fb1_turbopack-tests_tests_snapshot_imports_static-and-dynamic_input_vercel.mjs._.js" diff --git a/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_82c953.js b/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_82c953.js index 31d2abcff9f4c..e04824a94c61e 100644 --- a/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_82c953.js +++ b/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_82c953.js @@ -1,10 +1,10 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_82c953.js", { -"[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/vercel.svg (static)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/vercel.svg (static)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_export_value__("/crates/turbopack-tests/tests/snapshot/imports/static/static/957b9b162f8447f9.svg"); })()), -"[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$static$2f$input$2f$vercel$2e$svg__$28$static$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/vercel.svg (static)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -12,8 +12,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$static$2f$input$2f$vercel$2e$svg__$28$static$29__["default"]); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js"))) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_imports_static_input_index.js_f5704b._.json", ["output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/index.js (ecmascript)"); } ]); @@ -25,7 +26,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -56,6 +57,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -80,8 +140,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -133,6 +196,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -407,6 +493,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -548,7 +635,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -559,18 +646,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -587,7 +676,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -619,35 +708,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -660,10 +758,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -697,24 +800,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -737,22 +843,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -842,17 +1104,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -955,10 +1235,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -983,18 +1273,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1004,18 +1358,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1036,6 +1429,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1043,7 +1437,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js b/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js new file mode 100644 index 0000000000000..302f4069e0977 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js @@ -0,0 +1,1460 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/vercel.svg (static)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_export_value__("/crates/turbopack-tests/tests/snapshot/imports/static/static/957b9b162f8447f9.svg"); +})()), +"[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$static$2f$input$2f$vercel$2e$svg__$28$static$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/vercel.svg (static)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$imports$2f$static$2f$input$2f$vercel$2e$svg__$28$static$29__["default"]); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/crates_turbopack-tests_tests_snapshot_imports_static_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/imports/static/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js.map b/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js.map new file mode 100644 index 0000000000000..9f2ca3f475e0d --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/imports/static/output/crates_turbopack-tests_tests_snapshot_imports_static_input_index_9fc270.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 8, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/imports/static/input/index.js"],"sourcesContent":["import img from \"./vercel.svg\";\nconsole.log(img);\n"],"names":[],"mappings":";;;AACA,QAAQ,GAAG"}}, + {"offset": {"line": 12, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_4e764f.js b/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_4e764f.js index 34594288aa663..d45d4e066208d 100644 --- a/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_4e764f.js +++ b/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_4e764f.js @@ -1,14 +1,15 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_4e764f.js", { -"[project]/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__external__node$3a$fs__ = __turbopack_external_require__("node:fs", true); "__TURBOPACK__ecmascript__hoisting__location__"; ; })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js"))) return true; + registerChunkList("output/a587c_tests_snapshot_node_node_protocol_external_input_index.js_f5704b._.json", ["output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/input/index.js (ecmascript)"); } ]); @@ -20,7 +21,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -51,6 +52,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -75,8 +135,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -128,6 +191,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -402,6 +488,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -543,7 +630,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -554,18 +641,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -582,7 +671,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -614,35 +703,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -655,10 +753,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -692,24 +795,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -732,22 +838,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -837,17 +1099,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -950,10 +1230,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -978,18 +1268,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -999,18 +1353,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1031,6 +1424,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1038,7 +1432,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js b/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js new file mode 100644 index 0000000000000..e9dc42858f45e --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js @@ -0,0 +1,1455 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__external__node$3a$fs__ = __turbopack_external_require__("node:fs", true); +"__TURBOPACK__ecmascript__hoisting__location__"; +; + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/a587c_tests_snapshot_node_node_protocol_external_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js.map b/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js.map new file mode 100644 index 0000000000000..e998c77728e14 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/node/node_protocol_external/output/79fb1_turbopack-tests_tests_snapshot_node_node_protocol_external_input_index_69be78.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":""}}, + {"offset": {"line": 7, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/63a02_styled-components_index.js b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/63a02_styled-components_index.js index b21d071e4ccb4..8dccf61e5f92e 100644 --- a/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/63a02_styled-components_index.js +++ b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/63a02_styled-components_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/63a02_styled-components_index.js", { -"[project]/crates/turbopack-tests/tests/node_modules/styled-components/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/node_modules/styled-components/index.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { "purposefully empty stub"; "styled-components/index.js"; diff --git a/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js new file mode 100644 index 0000000000000..21dac7303a59e --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js @@ -0,0 +1,1462 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f$styled$2d$components$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/styled-components/index.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +const MyButton = __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f$styled$2d$components$2f$index$2e$js__$28$ecmascript$29__["default"].button.withConfig({ + displayName: "MyButton", + componentId: "sc-86737cfc-0" +})` + background: blue; +`; +console.log(MyButton); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_styled-components_index.js"))) return true; + registerChunkList("output/a587c_tests_snapshot_styled_components_styled_components_input_index.js_f5704b._.json", ["output/63a02_styled-components_index.js"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js.map b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js.map new file mode 100644 index 0000000000000..d8b322fd809ba --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/input/index.js"],"sourcesContent":["import styled from \"styled-components\";\n\nconst MyButton = styled.button`\n background: blue;\n`;\n\nconsole.log(MyButton);\n"],"names":[],"mappings":";;;AAEA,MAAM,WAAW,6KAAO,MAAM;;;EAAA,CAAC;;AAE/B,CAAC;AAED,QAAQ,GAAG,CAAC"}}, + {"offset": {"line": 14, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_0496ed.js b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_0496ed.js index f1a0d13516439..5885493148e28 100644 --- a/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_0496ed.js +++ b/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/output/a587c_tests_snapshot_styled_components_styled_components_input_index_0496ed.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_styled_components_styled_components_input_index_0496ed.js", { -"[project]/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f$styled$2d$components$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/styled-components/index.js (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -14,8 +14,9 @@ const MyButton = __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbo console.log(MyButton); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/63a02_styled-components_index.js") && loadedChunks.has("output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_styled-components_index.js") && loadedChunks.has("output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js"))) return true; + registerChunkList("output/a587c_tests_snapshot_styled_components_styled_components_input_index.js_f5704b._.json", ["output/63a02_styled-components_index.js","output/a587c_tests_snapshot_styled_components_styled_components_input_index_01a621.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/styled_components/styled_components/input/index.js (ecmascript)"); } ]); @@ -27,7 +28,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -58,6 +59,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -82,8 +142,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -135,6 +198,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -409,6 +495,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -550,7 +637,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -561,18 +648,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -589,7 +678,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -621,35 +710,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -662,10 +760,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -699,24 +802,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -739,22 +845,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -844,17 +1106,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -957,10 +1237,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -985,18 +1275,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1006,18 +1360,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1038,6 +1431,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1045,7 +1439,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/63a02_react_jsx-dev-runtime.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/63a02_react_jsx-dev-runtime.js index 0612b8265bb49..a213570a77fd4 100644 --- a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/63a02_react_jsx-dev-runtime.js +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/63a02_react_jsx-dev-runtime.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/63a02_react_jsx-dev-runtime.js", { -"[project]/crates/turbopack-tests/tests/node_modules/react/jsx-dev-runtime.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/node_modules/react/jsx-dev-runtime.js (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { "purposefully empty stub"; "react/jsx-dev-runtime.js"; diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/7b7bf_third_party_component_index.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/7b7bf_third_party_component_index.js index b4e78f94dd03b..3d00664f6ee27 100644 --- a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/7b7bf_third_party_component_index.js +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/7b7bf_third_party_component_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/7b7bf_third_party_component_index.js", { -"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/node_modules/third_party_component/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/node_modules/third_party_component/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "default": ()=>ThirdPartyComponent diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_2a30e9.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_2a30e9.js index 8839a8077796c..7a6da67f32bab 100644 --- a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_2a30e9.js +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_2a30e9.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_2a30e9.js", { -"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/app/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/app/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$packages$2f$component$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/component/index.js (ecmascript)"); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$node_modules$2f$third_party_component$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/node_modules/third_party_component/index.js (ecmascript)"); @@ -10,8 +10,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$packages$2f$component$2f$index$2e$js__$28$ecmascript$29__["default"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$node_modules$2f$third_party_component$2f$index$2e$js__$28$ecmascript$29__["default"]); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/63a02_react_jsx-dev-runtime.js") && loadedChunks.has("output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js") && loadedChunks.has("output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js") && loadedChunks.has("output/7b7bf_third_party_component_index.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_react_jsx-dev-runtime.js") && loadedChunks.has("output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js") && loadedChunks.has("output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js") && loadedChunks.has("output/7b7bf_third_party_component_index.js"))) return true; + registerChunkList("output/8562f_snapshot_swc_transforms_mono_transforms_input_packages_app_index.js_f5704b._.json", ["output/63a02_react_jsx-dev-runtime.js","output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js","output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js","output/7b7bf_third_party_component_index.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/app/index.js (ecmascript)"); } ]); @@ -23,7 +24,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -54,6 +55,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -78,8 +138,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -131,6 +194,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -405,6 +491,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -546,7 +633,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -557,18 +644,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -585,7 +674,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -617,35 +706,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -658,10 +756,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -695,24 +798,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -735,22 +841,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -840,17 +1102,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -953,10 +1233,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -981,18 +1271,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1002,18 +1356,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1034,6 +1427,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1041,7 +1435,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js new file mode 100644 index 0000000000000..814ac88c0b810 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js @@ -0,0 +1,1458 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/app/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$packages$2f$component$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/component/index.js (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$node_modules$2f$third_party_component$2f$index$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/node_modules/third_party_component/index.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$packages$2f$component$2f$index$2e$js__$28$ecmascript$29__["default"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$swc_transforms$2f$mono_transforms$2f$input$2f$node_modules$2f$third_party_component$2f$index$2e$js__$28$ecmascript$29__["default"]); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_react_jsx-dev-runtime.js") && loadedChunks.has("output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js") && loadedChunks.has("output/7b7bf_third_party_component_index.js"))) return true; + registerChunkList("output/8562f_snapshot_swc_transforms_mono_transforms_input_packages_app_index.js_f5704b._.json", ["output/63a02_react_jsx-dev-runtime.js","output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js","output/7b7bf_third_party_component_index.js"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/app/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js.map b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js.map new file mode 100644 index 0000000000000..b2a1e5637adfd --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_app_index_38ad57.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/app/index.js"],"sourcesContent":["import MyApp from \"component\";\nimport ThirdPartyComponent from \"third_party_component\";\n\nconsole.log(MyApp, ThirdPartyComponent);\n"],"names":[],"mappings":";;;;;AAGA,QAAQ,GAAG"}}, + {"offset": {"line": 10, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js index a574bc27f94bc..38d322db6db67 100644 --- a/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_swc_transforms_mono_transforms_input_packages_component_index.js", { -"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/component/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/mono_transforms/input/packages/component/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "default": ()=>MyApp diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/63a02_@swc_helpers_src__class_call_check.mjs._.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/63a02_@swc_helpers_src__class_call_check.mjs._.js index c90a8b0be8c88..fcaab5e5dae5a 100644 --- a/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/63a02_@swc_helpers_src__class_call_check.mjs._.js +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/63a02_@swc_helpers_src__class_call_check.mjs._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/63a02_@swc_helpers_src__class_call_check.mjs._.js", { -"[project]/crates/turbopack-tests/tests/node_modules/@swc/helpers/src/_class_call_check.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { +"[project]/crates/turbopack-tests/tests/node_modules/@swc/helpers/src/_class_call_check.mjs (ecmascript)": (function({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname, m: module, e: exports }) { !function() { "purposefully empty stub"; "@swc/helpers/src/_class_call_check.mjs"; diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_097653.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_097653.js index 84b577e0fa121..f115ae0243411 100644 --- a/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_097653.js +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_097653.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_097653.js", { -"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$swc$2f$helpers$2f$src$2f$_class_call_check$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/@swc/helpers/src/_class_call_check.mjs (ecmascript)"); "__TURBOPACK__ecmascript__hoisting__location__"; @@ -13,8 +13,9 @@ var Foo = function Foo() { console.log(Foo, [].includes("foo")); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/63a02_@swc_helpers_src__class_call_check.mjs._.js") && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_@swc_helpers_src__class_call_check.mjs._.js") && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index.js_f5704b._.json", ["output/63a02_@swc_helpers_src__class_call_check.mjs._.js","output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/input/index.js (ecmascript)"); } ]); @@ -26,7 +27,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -57,6 +58,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -81,8 +141,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -134,6 +197,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -408,6 +494,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -549,7 +636,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -560,18 +647,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -588,7 +677,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -620,35 +709,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -661,10 +759,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -698,24 +801,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -738,22 +844,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -843,17 +1105,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -956,10 +1236,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -984,18 +1274,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1005,18 +1359,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1037,6 +1430,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1044,7 +1438,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js new file mode 100644 index 0000000000000..6a3d444ee3e44 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js @@ -0,0 +1,1461 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$swc$2f$helpers$2f$src$2f$_class_call_check$2e$mjs__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/node_modules/@swc/helpers/src/_class_call_check.mjs (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +; +var Foo = function Foo() { + "use strict"; + __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$node_modules$2f40$swc$2f$helpers$2f$src$2f$_class_call_check$2e$mjs__$28$ecmascript$29__["default"](this, Foo); +}; +console.log(Foo, [].includes("foo")); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/63a02_@swc_helpers_src__class_call_check.mjs._.js"))) return true; + registerChunkList("output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index.js_f5704b._.json", ["output/63a02_@swc_helpers_src__class_call_check.mjs._.js"]); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js.map b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js.map new file mode 100644 index 0000000000000..1e6e076dfc14f --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/output/79fb1_turbopack-tests_tests_snapshot_swc_transforms_preset_env_input_index_62f043.js.map @@ -0,0 +1,6 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/swc_transforms/preset_env/input/index.js"],"sourcesContent":["class Foo {}\n\nconsole.log(Foo, [].includes(\"foo\"));\n"],"names":[],"mappings":";;;;AAAA,IAAA,AAAM,MAAN,SAAM;;uMAAA;;AAEN,QAAQ,GAAG,CAAC,KAAK,EAAE,CAAC,QAAQ,CAAC"}}, + {"offset": {"line": 13, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js b/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js new file mode 100644 index 0000000000000..179cdd95badb6 --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js @@ -0,0 +1,1468 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_esm__({ + "prop": ()=>prop +}); +const prop = 1; + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__["prop"]); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/a587c_tests_snapshot_typescript_jsconfig-baseurl_input_index.js_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/index.js (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js.map b/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js.map new file mode 100644 index 0000000000000..76729a5945d3e --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js"],"sourcesContent":["export const prop = 1;\n"],"names":[],"mappings":";;;AAAO,MAAM,OAAO"}}, + {"offset": {"line": 8, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 12, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/index.js"],"sourcesContent":["import { prop as globalFoo } from \"foo\";\nimport { prop as localFoo } from \"./foo\";\nimport { prop as atFoo } from \"@/foo\";\n\nconsole.log(globalFoo, localFoo, atFoo);\n"],"names":[],"mappings":";;;;;;;AAIA,QAAQ,GAAG"}}, + {"offset": {"line": 20, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_3d21b6.js b/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_3d21b6.js index 40ea89936a71e..41f37160094ff 100644 --- a/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_3d21b6.js +++ b/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_3d21b6.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_3d21b6.js", { -"[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "prop": ()=>prop @@ -8,7 +8,7 @@ __turbopack_esm__({ const prop = 1; })()), -"[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/index.js (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)"); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/prop.js (ecmascript)"); @@ -20,8 +20,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$jsconfig$2d$baseurl$2f$input$2f$prop$2e$js__$28$ecmascript$29__["prop"]); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js"))) return true; + registerChunkList("output/a587c_tests_snapshot_typescript_jsconfig-baseurl_input_index.js_f5704b._.json", ["output/79fb1_turbopack-tests_tests_snapshot_typescript_jsconfig-baseurl_input_index_18c34e.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/typescript/jsconfig-baseurl/input/index.js (ecmascript)"); } ]); @@ -33,7 +34,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -64,6 +65,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -88,8 +148,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -141,6 +204,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -415,6 +501,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -556,7 +643,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -567,18 +654,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -595,7 +684,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -627,35 +716,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -668,10 +766,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -705,24 +808,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -745,22 +851,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -850,17 +1112,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -963,10 +1243,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -991,18 +1281,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1012,18 +1366,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1044,6 +1437,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1051,7 +1445,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory; diff --git a/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js b/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js new file mode 100644 index 0000000000000..656451ea1381c --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js @@ -0,0 +1,1468 @@ +(self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js", { + +"[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +__turbopack_esm__({ + "prop": ()=>prop +}); +const prop = 1; + +})()), +"[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/index.ts (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { + +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)"); +var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)"); +"__TURBOPACK__ecmascript__hoisting__location__"; +; +; +; +console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__["prop"]); + +})()), +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true)) return true; + registerChunkList("output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_f5704b._.json", []); + instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/index.ts (ecmascript)"); +} +]); +(() => { +if (!Array.isArray(globalThis.TURBOPACK)) { + return; +} +/** @typedef {import('../types/backend').RuntimeBackend} RuntimeBackend */ + +/** @type {RuntimeBackend} */ +const BACKEND = { + loadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (chunkPath.endsWith(".css")) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + document.body.appendChild(link); + } else if (chunkPath.endsWith(".js")) { + const script = document.createElement("script"); + script.src = `/${chunkPath}`; + // We'll only mark the chunk as loaded once the script has been executed, + // which happens in `registerChunk`. Hence the absence of `resolve()` in + // this branch. + script.onerror = () => { + reject(); + }; + document.body.appendChild(script); + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }); + }, + + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + + restart: () => self.location.reload(), +}; +/* eslint-disable @next/next/no-assign-module-variable */ + +/** @typedef {import('../types').ChunkRegistration} ChunkRegistration */ +/** @typedef {import('../types').ModuleFactory} ModuleFactory */ + +/** @typedef {import('../types').ChunkPath} ChunkPath */ +/** @typedef {import('../types').ModuleId} ModuleId */ +/** @typedef {import('../types').GetFirstModuleChunk} GetFirstModuleChunk */ + +/** @typedef {import('../types').Module} Module */ +/** @typedef {import('../types').Exports} Exports */ +/** @typedef {import('../types').EsmInteropNamespace} EsmInteropNamespace */ +/** @typedef {import('../types').Runnable} Runnable */ + +/** @typedef {import('../types').Runtime} Runtime */ + +/** @typedef {import('../types').RefreshHelpers} RefreshHelpers */ +/** @typedef {import('../types/hot').Hot} Hot */ +/** @typedef {import('../types/hot').HotData} HotData */ +/** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ +/** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ +/** @typedef {import('../types/hot').HotState} HotState */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ + +/** @typedef {import('../types/runtime').Loader} Loader */ +/** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ + +/** @type {Array} */ +let runnable = []; +/** @type {Object.} */ +const moduleFactories = { __proto__: null }; +/** @type {Object.} */ +const moduleCache = { __proto__: null }; +/** + * Contains the IDs of all chunks that have been loaded. + * + * @type {Set} + */ +const loadedChunks = new Set(); +/** + * Maps a chunk ID to the chunk's loader if the chunk is currently being loaded. + * + * @type {Map} + */ +const chunkLoaders = new Map(); +/** + * Maps module IDs to persisted data between executions of their hot module + * implementation (`hot.data`). + * + * @type {Map} + */ +const moduleHotData = new Map(); +/** + * Maps module instances to their hot module state. + * + * @type {Map} + */ +const moduleHotState = new Map(); +/** + * Module IDs that are instantiated as part of the runtime of a chunk. + * + * @type {Set} + */ +const runtimeModules = new Set(); +/** + * Map from module ID to the chunks that contain this module. + * + * In HMR, we need to keep track of which modules are contained in which so + * chunks. This is so we don't eagerly dispose of a module when it is removed + * from chunk A, but still exists in chunk B. + * + * @type {Map>} + */ +const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + +const hOP = Object.prototype.hasOwnProperty; +const _process = + typeof process !== "undefined" + ? process + : { + env: {}, + // Some modules rely on `process.browser` to execute browser-specific code. + // NOTE: `process.browser` is specific to Webpack. + browser: true, + }; + +const toStringTag = typeof Symbol !== "undefined" && Symbol.toStringTag; + +/** + * @param {any} obj + * @param {PropertyKey} name + * @param {PropertyDescriptor & ThisType} options + */ +function defineProp(obj, name, options) { + if (!hOP.call(obj, name)) Object.defineProperty(obj, name, options); +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record any>} getters + */ +function esm(exports, getters) { + defineProp(exports, "__esModule", { value: true }); + if (toStringTag) defineProp(exports, toStringTag, { value: "Module" }); + for (const key in getters) { + defineProp(exports, key, { get: getters[key], enumerable: true }); + } +} + +/** + * Adds the getters to the exports object + * + * @param {Exports} exports + * @param {Record} props + */ +function cjs(exports, props) { + for (const key in props) { + defineProp(exports, key, { get: () => props[key], enumerable: true }); + } +} + +/** + * @param {Module} module + * @param {any} value + */ +function exportValue(module, value) { + module.exports = value; +} + +/** + * @param {Record} obj + * @param {string} key + */ +function createGetter(obj, key) { + return () => obj[key]; +} + +/** + * @param {Exports} raw + * @param {EsmInteropNamespace} ns + * @param {boolean} [allowExportDefault] + */ +function interopEsm(raw, ns, allowExportDefault) { + /** @type {Object. any>} */ + const getters = { __proto__: null }; + for (const key in raw) { + getters[key] = createGetter(raw, key); + } + if (!(allowExportDefault && "default" in getters)) { + getters["default"] = () => raw; + } + esm(ns, getters); +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @param {boolean} allowExportDefault + * @returns {EsmInteropNamespace} + */ +function esmImport(sourceModule, id, allowExportDefault) { + const module = getOrInstantiateModuleFromParent(id, sourceModule); + const raw = module.exports; + if (raw.__esModule) return raw; + if (module.interopNamespace) return module.interopNamespace; + const ns = (module.interopNamespace = {}); + interopEsm(raw, ns, allowExportDefault); + return ns; +} + +/** + * @param {Module} sourceModule + * @param {ModuleId} id + * @returns {Exports} + */ +function commonJsRequire(sourceModule, id) { + return getOrInstantiateModuleFromParent(id, sourceModule).exports; +} + +function externalRequire(id, esm) { + let raw; + try { + raw = require(id); + } catch (err) { + // TODO(alexkirsz) This can happen when a client-side module tries to load + // an external module we don't provide a shim for (e.g. querystring, url). + // For now, we fail semi-silently, but in the future this should be a + // compilation error. + throw new Error(`Failed to load external module ${id}: ${err}`); + } + if (!esm || raw.__esModule) { + return raw; + } + const ns = {}; + interopEsm(raw, ns, true); + return ns; +} +externalRequire.resolve = (name, opt) => { + return require.resolve(name, opt); +}; + +/** + * @param {ModuleId} from + * @param {string} chunkPath + * @returns {Promise | undefined} + */ +function loadChunk(from, chunkPath) { + if (loadedChunks.has(chunkPath)) { + return Promise.resolve(); + } + + const chunkLoader = getOrCreateChunkLoader(chunkPath, from); + + return chunkLoader.promise; +} + +/** + * @param {string} chunkPath + * @param {ModuleId} from + * @returns {Loader} + */ +function getOrCreateChunkLoader(chunkPath, from) { + let chunkLoader = chunkLoaders.get(chunkPath); + if (chunkLoader) { + return chunkLoader; + } + + let resolve; + let reject; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + + const onError = (error) => { + chunkLoaders.delete(chunkPath); + reject( + new Error( + `Failed to load chunk from ${chunkPath}${error ? `: ${error}` : ""}` + ) + ); + }; + + const onLoad = () => { + loadedChunks.add(chunkPath); + chunkLoaders.delete(chunkPath); + resolve(); + }; + + chunkLoader = { + promise, + onLoad, + }; + chunkLoaders.set(chunkPath, chunkLoader); + + BACKEND.loadChunk(chunkPath, from).then(onLoad, onError); + + return chunkLoader; +} + +/** + * @enum {number} + */ +const SourceType = { + /** + * The module was instantiated because it was included in an evaluated chunk's + * runtime. + */ + Runtime: 0, + /** + * The module was instantiated because a parent module imported it. + */ + Parent: 1, + /** + * The module was instantiated because it was included in a chunk's hot module + * update. + */ + Update: 2, +}; + +/** + * + * @param {ModuleId} id + * @param {SourceType} sourceType + * @param {ModuleId} [sourceId] + * @returns {Module} + */ +function instantiateModule(id, sourceType, sourceId) { + const moduleFactory = moduleFactories[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. + let instantiationReason; + switch (sourceType) { + case SourceType.Runtime: + instantiationReason = "as a runtime entry"; + break; + case SourceType.Parent: + instantiationReason = `because it was required from module ${sourceId}`; + break; + case SourceType.Update: + instantiationReason = "because of an HMR update"; + break; + } + throw new Error( + `Module ${id} was instantiated ${instantiationReason}, but the module factory is not available. It might have been deleted in an HMR update.` + ); + } + + const hotData = moduleHotData.get(id); + const { hot, hotState } = createModuleHot(hotData); + + /** @type {Module} */ + const module = { + exports: {}, + loaded: false, + id, + parents: [], + children: [], + interopNamespace: undefined, + hot, + }; + moduleCache[id] = module; + moduleHotState.set(module, hotState); + + if (sourceType === SourceType.Runtime) { + runtimeModules.add(id); + } else if (sourceType === SourceType.Parent) { + module.parents.push(sourceId); + + // No need to add this module as a child of the parent module here, this + // has already been taken care of in `getOrInstantiateModuleFromParent`. + } + + runModuleExecutionHooks(module, () => { + moduleFactory.call(module.exports, { + e: module.exports, + r: commonJsRequire.bind(null, module), + x: externalRequire, + i: esmImport.bind(null, module), + s: esm.bind(null, module.exports), + j: cjs.bind(null, module.exports), + v: exportValue.bind(null, module), + m: module, + c: moduleCache, + l: loadChunk.bind(null, id), + k: registerChunkList, + p: _process, + g: globalThis, + __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), + }); + }); + + module.loaded = true; + if (module.interopNamespace) { + // in case of a circular dependency: cjs1 -> esm2 -> cjs1 + interopEsm(module.exports, module.interopNamespace); + } + + return module; +} + +/** + * NOTE(alexkirsz) Webpack has an "module execution" interception hook that + * Next.js' React Refresh runtime hooks into to add module context to the + * refresh registry. + * + * @param {Module} module + * @param {() => void} executeModule + */ +function runModuleExecutionHooks(module, executeModule) { + const cleanupReactRefreshIntercept = + typeof globalThis.$RefreshInterceptModuleExecution$ === "function" + ? globalThis.$RefreshInterceptModuleExecution$(module.id) + : () => {}; + + executeModule(); + + if ("$RefreshHelpers$" in globalThis) { + // This pattern can also be used to register the exports of + // a module with the React Refresh runtime. + registerExportsAndSetupBoundaryForReactRefresh( + module, + globalThis.$RefreshHelpers$ + ); + } + + cleanupReactRefreshIntercept(); +} + +/** + * Retrieves a module from the cache, or instantiate it if it is not cached. + * + * @param {ModuleId} id + * @param {Module} sourceModule + * @returns {Module} + */ +function getOrInstantiateModuleFromParent(id, sourceModule) { + if (!sourceModule.hot.active) { + console.warn( + `Unexpected import of module ${id} from module ${sourceModule.id}, which was deleted by an HMR update` + ); + } + + const module = moduleCache[id]; + + if (sourceModule.children.indexOf(id) === -1) { + sourceModule.children.push(id); + } + + if (module) { + if (module.parents.indexOf(sourceModule.id) === -1) { + module.parents.push(sourceModule.id); + } + + return module; + } + + return instantiateModule(id, SourceType.Parent, sourceModule.id); +} + +/** + * This is adapted from https://github.com/vercel/next.js/blob/3466862d9dc9c8bb3131712134d38757b918d1c0/packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts + * + * @param {Module} module + * @param {RefreshHelpers} helpers + */ +function registerExportsAndSetupBoundaryForReactRefresh(module, helpers) { + const currentExports = module.exports; + const prevExports = module.hot.data.prevExports ?? null; + + helpers.registerExportsForReactRefresh(currentExports, module.id); + + // A module can be accepted automatically based on its exports, e.g. when + // it is a Refresh Boundary. + if (helpers.isReactRefreshBoundary(currentExports)) { + // Save the previous exports on update so we can compare the boundary + // signatures. + module.hot.dispose((data) => { + data.prevExports = currentExports; + }); + // Unconditionally accept an update to this module, we'll check if it's + // still a Refresh Boundary later. + module.hot.accept(); + + // This field is set when the previous version of this module was a + // Refresh Boundary, letting us know we need to check for invalidation or + // enqueue an update. + if (prevExports !== null) { + // A boundary can become ineligible if its exports are incompatible + // with the previous exports. + // + // For example, if you add/remove/change exports, we'll want to + // re-execute the importing modules, and force those components to + // re-render. Similarly, if you convert a class component to a + // function, we want to invalidate the boundary. + if ( + helpers.shouldInvalidateReactRefreshBoundary( + prevExports, + currentExports + ) + ) { + module.hot.invalidate(); + } else { + helpers.scheduleUpdate(); + } + } + } else { + // Since we just executed the code for the module, it's possible that the + // new exports made it ineligible for being a boundary. + // We only care about the case when we were _previously_ a boundary, + // because we already accepted this update (accidental side effect). + const isNoLongerABoundary = prevExports !== null; + if (isNoLongerABoundary) { + module.hot.invalidate(); + } + } +} + +/** + * @param {ModuleId[]} dependencyChain + * @returns {string} + */ +function formatDependencyChain(dependencyChain) { + return `Dependency chain: ${dependencyChain.join(" -> ")}`; +} + +/** + * @param {EcmascriptModuleEntry} entry + * @returns {ModuleFactory} + * @private + */ +function _eval({ code, url, map }) { + code += `\n\n//# sourceURL=${location.origin}${url}`; + if (map) code += `\n//# sourceMappingURL=${map}`; + return eval(code); +} + +/** + * @param {Map} added + * @param {Map} modified + * @param {Record} code + * @returns {{outdatedModules: Set, newModuleFactories: Map}} + */ +function computeOutdatedModules(added, modified, code) { + const outdatedModules = new Set(); + const newModuleFactories = new Map(); + + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); + } + + for (const [moduleId, entry] of modified) { + const effect = getAffectedModuleEffects(moduleId); + + switch (effect.type) { + case "unaccepted": + throw new Error( + `cannot apply update: unaccepted module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "self-declined": + throw new Error( + `cannot apply update: self-declined module. ${formatDependencyChain( + effect.dependencyChain + )}.` + ); + case "accepted": + newModuleFactories.set(moduleId, _eval(entry)); + for (const outdatedModuleId of effect.outdatedModules) { + outdatedModules.add(outdatedModuleId); + } + break; + // TODO(alexkirsz) Dependencies: handle dependencies effects. + } + } + + return { outdatedModules, newModuleFactories }; +} + +/** + * @param {Iterable} outdatedModules + * @returns {{ moduleId: ModuleId, errorHandler: true | Function }[]} + */ +function computeOutdatedSelfAcceptedModules(outdatedModules) { + const outdatedSelfAcceptedModules = []; + for (const moduleId of outdatedModules) { + const module = moduleCache[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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} + */ +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); + } + } + + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } + } + } + + return { disposedModules }; +} + +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } + + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); + } + + // TODO(alexkirsz) Dependencies: remove outdated dependency from module + // children. +} + +/** + * Disposes of an instance of a module. + * + * Returns the persistent hot data that should be kept for the next module + * instance. + * + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode + */ +function disposeModule(moduleId, mode) { + const module = moduleCache[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; + + delete moduleCache[module.id]; + moduleHotState.delete(module); + + // TODO(alexkirsz) Dependencies: delete the module from outdated deps. + + // Remove the disposed module from its children's parents list. + // It will be added back once the module re-instantiates and imports its + // children again. + for (const childId of module.children) { + const child = moduleCache[childId]; + if (!child) { + continue; + } + + const idx = child.parents.indexOf(module.id); + if (idx >= 0) { + child.parents.splice(idx, 1); + } + } + + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } +} + +/** + * + * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules + * @param {Map} newModuleFactories + */ +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { + // Update module factories. + for (const [moduleId, factory] of newModuleFactories.entries()) { + moduleFactories[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); + } catch (err) { + if (typeof errorHandler === "function") { + try { + errorHandler(err, { moduleId, module: moduleCache[moduleId] }); + } catch (_) { + // Ignore error. + } + } + } + } +} + +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update + */ +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } + + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} + +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); + const outdatedSelfAcceptedModules = + computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} + +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; +} + +/** + * + * @param {ModuleId} moduleId + * @returns {ModuleEffect} + */ +function getAffectedModuleEffects(moduleId) { + const outdatedModules = new Set(); + + /** @typedef {{moduleId?: ModuleId, dependencyChain: ModuleId[]}} QueueItem */ + + /** @type {QueueItem[]} */ + const queue = [ + { + moduleId, + dependencyChain: [], + }, + ]; + + while (queue.length > 0) { + const { moduleId, dependencyChain } = + /** @type {QueueItem} */ queue.shift(); + 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 = moduleCache[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 = moduleCache[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, + }; +} + +/** + * @param {ChunkPath} chunkListPath + * @param {import('../types/protocol').ServerMessage} update + */ +function handleApply(chunkListPath, update) { + switch (update.type) { + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); + break; + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. + BACKEND.restart(); + break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } + default: + throw new Error(`Unknown update type: ${update.type}`); + } +} + +/** + * @param {HotData} [hotData] + * @returns {{hotState: HotState, hot: Hot}} + */ +function createModuleHot(hotData) { + /** @type {HotState} */ + const hotState = { + selfAccepted: false, + selfDeclined: false, + selfInvalidated: false, + disposeHandlers: [], + }; + + /** + * TODO(alexkirsz) Support full (dep, callback, errorHandler) form. + * + * @param {string | string[] | AcceptErrorHandler} [dep] + * @param {AcceptCallback} [_callback] + * @param {AcceptErrorHandler} [_errorHandler] + */ + function accept(dep, _callback, _errorHandler) { + if (dep === undefined) { + hotState.selfAccepted = true; + } else if (typeof dep === "function") { + hotState.selfAccepted = dep; + } else { + throw new Error("unsupported `accept` signature"); + } + } + + /** @type {Hot} */ + const 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 ?? {}, + + accept: accept, + + 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; + // TODO(alexkirsz) The original HMR code had management-related code + // here. + }, + + // 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) => {}, + }; + + return { hot, hotState }; +} + +/** + * Adds a module to a chunk. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + */ +function addModuleToChunk(moduleId, chunkPath) { + let moduleChunks = moduleChunksMap.get(moduleId); + if (!moduleChunks) { + moduleChunks = new Set([chunkPath]); + moduleChunksMap.set(moduleId, moduleChunks); + } else { + moduleChunks.add(chunkPath); + } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } +} + +/** + * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. + * + * @type {GetFirstModuleChunk} + */ +function getFirstModuleChunk(moduleId) { + const moduleChunkPaths = moduleChunksMap.get(moduleId); + if (moduleChunkPaths == null) { + return null; + } + + return moduleChunkPaths.values().next().value; +} + +/** + * Removes a module from a chunk. Returns true there are no remaining chunks + * including this module. + * + * @param {ModuleId} moduleId + * @param {ChunkPath} chunkPath + * @returns {boolean} + */ +function removeModuleFromChunk(moduleId, chunkPath) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { + return false; + } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } + + return true; +} + +/** + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. + */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + +/** + * Instantiates a runtime module. + * + * @param {ModuleId} moduleId + * @returns {Module} + */ +function instantiateRuntimeModule(moduleId) { + return instantiateModule(moduleId, SourceType.Runtime); +} + +/** + * Subscribes to chunk list updates from the update server and applies them. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkList(chunkListPath, chunkPaths) { + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ + chunkListPath, + handleApply.bind(null, chunkListPath), + ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); +} + +/** + * @param {ChunkPath} chunkPath + */ +function markChunkAsLoaded(chunkPath) { + const chunkLoader = chunkLoaders.get(chunkPath); + if (!chunkLoader) { + loadedChunks.add(chunkPath); + + // This happens for all initial chunks that are loaded directly from + // the HTML. + return; + } + + // Only chunks that are loaded via `loadChunk` will have a loader. + chunkLoader.onLoad(); +} + +/** @type {Runtime} */ +const runtime = { + loadedChunks, + modules: moduleFactories, + cache: moduleCache, + instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, +}; + +/** + * @param {ChunkRegistration} chunkRegistration + */ +function registerChunk([chunkPath, chunkModules, ...run]) { + markChunkAsLoaded(chunkPath); + for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { + if (!moduleFactories[moduleId]) { + moduleFactories[moduleId] = moduleFactory; + } + addModuleToChunk(moduleId, chunkPath); + } + runnable.push(...run); + runnable = runnable.filter((r) => r(runtime)); +} + +globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = + globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS || []; + +globalThis.TURBOPACK.forEach(registerChunk); +globalThis.TURBOPACK = { + push: registerChunk, +}; +})(); + + +//# sourceMappingURL=a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js.map \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js.map b/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js.map new file mode 100644 index 0000000000000..e0343cefa513e --- /dev/null +++ b/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js.map @@ -0,0 +1,8 @@ +{ + "version": 3, + "sections": [ + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts"],"sourcesContent":["export const prop = 1;\n"],"names":[],"mappings":";;;AAAO,MAAM,OAAO"}}, + {"offset": {"line": 8, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}, + {"offset": {"line": 12, "column": 0}, "map": {"version":3,"sources":["/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/index.ts"],"sourcesContent":["import { prop as globalFoo } from \"foo\";\nimport { prop as localFoo } from \"./foo\";\nimport { prop as atFoo } from \"@/foo\";\n\nconsole.log(globalFoo, localFoo, atFoo);\n"],"names":[],"mappings":";;;;;;;AAIA,QAAQ,GAAG"}}, + {"offset": {"line": 20, "column": 0}, "map": {"version":3,"sources":[],"names":[],"mappings":"A"}}] +} \ No newline at end of file diff --git a/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_814f4c._.js b/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_814f4c._.js index 00fc62e2e7bd4..fdd957f8758c5 100644 --- a/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_814f4c._.js +++ b/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_814f4c._.js @@ -1,6 +1,6 @@ (self.TURBOPACK = self.TURBOPACK || []).push(["output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_814f4c._.js", { -"[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { __turbopack_esm__({ "prop": ()=>prop @@ -8,7 +8,7 @@ __turbopack_esm__({ const prop = 1; })()), -"[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/index.ts (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { +"[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/index.ts (ecmascript)": (({ r: __turbopack_require__, x: __turbopack_external_require__, i: __turbopack_import__, s: __turbopack_esm__, v: __turbopack_export_value__, c: __turbopack_cache__, l: __turbopack_load__, k: __turbopack_register_chunk_list__, j: __turbopack_cjs__, p: process, g: global, __dirname }) => (() => { var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)"); var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__ = __turbopack_import__("[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/prop.ts (ecmascript)"); @@ -20,8 +20,9 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests console.log(__TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__["prop"], __TURBOPACK__imported__module__$5b$project$5d2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$typescript$2f$tsconfig$2d$baseurl$2f$input$2f$prop$2e$ts__$28$ecmascript$29__["prop"]); })()), -}, ({ loadedChunks, instantiateRuntimeModule }) => { - if(!(true && loadedChunks.has("output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js"))) return true; +}, ({ loadedChunks, instantiateRuntimeModule, registerChunkList }) => { + if (!(true && loadedChunks.has("output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js"))) return true; + registerChunkList("output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_f5704b._.json", ["output/a587c_tests_snapshot_typescript_tsconfig-baseurl_input_index.ts_540b0c._.js"]); instantiateRuntimeModule("[project]/crates/turbopack-tests/tests/snapshot/typescript/tsconfig-baseurl/input/index.ts (ecmascript)"); } ]); @@ -33,7 +34,7 @@ if (!Array.isArray(globalThis.TURBOPACK)) { /** @type {RuntimeBackend} */ const BACKEND = { - loadChunk(chunkPath, _from) { + loadChunk(chunkPath) { return new Promise((resolve, reject) => { if (chunkPath.endsWith(".css")) { const link = document.createElement("link"); @@ -64,6 +65,65 @@ const BACKEND = { }); }, + unloadChunk(chunkPath) { + if (chunkPath.endsWith(".css")) { + const links = document.querySelectorAll(`link[href="/${chunkPath}"]`); + for (const link of Array.from(links)) { + link.remove(); + } + } else if (chunkPath.endsWith(".js")) { + // Unloading a JS chunk would have no effect, as it lives in the JS + // runtime once evaluated. + // However, we still want to remove the script tag from the DOM to keep + // the HTML somewhat consistent from the user's perspective. + const scripts = document.querySelectorAll(`script[src="/${chunkPath}"]`); + for (const script of Array.from(scripts)) { + script.remove(); + } + } else { + throw new Error(`can't infer type of chunk from path ${chunkPath}`); + } + }, + + reloadChunk(chunkPath) { + return new Promise((resolve, reject) => { + if (!chunkPath.endsWith(".css")) { + reject(new Error("The DOM backend can only reload CSS chunks")); + return; + } + + const previousLink = document.querySelector( + `link[href^="/${chunkPath}"]` + ); + + if (previousLink == null) { + reject(new Error(`No link element found for chunk ${chunkPath}`)); + return; + } + + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = `/${chunkPath}?t=${Date.now()}`; + link.onerror = () => { + reject(); + }; + link.onload = () => { + // First load the new CSS, then remove the old one. This prevents visible + // flickering that would happen in-between removing the previous CSS and + // loading the new one. + previousLink.remove(); + + // CSS chunks do not register themselves, and as such must be marked as + // loaded instantly. + resolve(); + }; + + // Make sure to insert the new CSS right after the previous one, so that + // its precedence is higher. + previousLink.parentElement.insertBefore(link, previousLink.nextSibling); + }); + }, + restart: () => self.location.reload(), }; /* eslint-disable @next/next/no-assign-module-variable */ @@ -88,8 +148,11 @@ const BACKEND = { /** @typedef {import('../types/hot').AcceptCallback} AcceptCallback */ /** @typedef {import('../types/hot').AcceptErrorHandler} AcceptErrorHandler */ /** @typedef {import('../types/hot').HotState} HotState */ -/** @typedef {import('../types/protocol').EcmascriptChunkUpdate} EcmascriptChunkUpdate */ -/** @typedef {import('../types/protocol').HmrUpdateEntry} HmrUpdateEntry */ +/** @typedef {import('../types/protocol').PartialUpdate} PartialUpdate */ +/** @typedef {import('../types/protocol').ChunkListUpdate} ChunkListUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedUpdate} EcmascriptMergedUpdate */ +/** @typedef {import('../types/protocol').EcmascriptMergedChunkUpdate} EcmascriptMergedChunkUpdate */ +/** @typedef {import('../types/protocol').EcmascriptModuleEntry} EcmascriptModuleEntry */ /** @typedef {import('../types/runtime').Loader} Loader */ /** @typedef {import('../types/runtime').ModuleEffect} ModuleEffect */ @@ -141,6 +204,29 @@ const runtimeModules = new Set(); * @type {Map>} */ const moduleChunksMap = new Map(); +/** + * Map from chunk path to all modules it contains. + * @type {Map>} + */ +const chunkModulesMap = new Map(); +/** + * Chunk lists that contain a runtime. When these chunk lists receive an update + * that can't be reconciled with the current state of the page, we need to + * reload the runtime entirely. + * @type {Set} + */ +const runtimeChunkLists = new Set(); +/** + * Map from chunk list to the chunk paths it contains. + * @type {Map>} + */ +const chunkListChunksMap = new Map(); +/** + * Map from chunk path to the chunk lists it belongs to. + * @type {Map>} + */ +const chunkChunkListsMap = new Map(); + const hOP = Object.prototype.hasOwnProperty; const _process = typeof process !== "undefined" @@ -415,6 +501,7 @@ function instantiateModule(id, sourceType, sourceId) { m: module, c: moduleCache, l: loadChunk.bind(null, id), + k: registerChunkList, p: _process, g: globalThis, __dirname: module.id.replace(/(^|\/)[\/]+$/, ""), @@ -556,7 +643,7 @@ function formatDependencyChain(dependencyChain) { } /** - * @param {HmrUpdateEntry} factory + * @param {EcmascriptModuleEntry} entry * @returns {ModuleFactory} * @private */ @@ -567,18 +654,20 @@ function _eval({ code, url, map }) { } /** - * @param {EcmascriptChunkUpdate} update + * @param {Map} added + * @param {Map} modified + * @param {Record} code * @returns {{outdatedModules: Set, newModuleFactories: Map}} */ -function computeOutdatedModules(update) { +function computeOutdatedModules(added, modified, code) { const outdatedModules = new Set(); const newModuleFactories = new Map(); - for (const [moduleId, factory] of Object.entries(update.added)) { - newModuleFactories.set(moduleId, _eval(factory)); + for (const [moduleId, entry] of added) { + newModuleFactories.set(moduleId, _eval(entry)); } - for (const [moduleId, factory] of Object.entries(update.modified)) { + for (const [moduleId, entry] of modified) { const effect = getAffectedModuleEffects(moduleId); switch (effect.type) { @@ -595,7 +684,7 @@ function computeOutdatedModules(update) { )}.` ); case "accepted": - newModuleFactories.set(moduleId, _eval(factory)); + newModuleFactories.set(moduleId, _eval(entry)); for (const outdatedModuleId of effect.outdatedModules) { outdatedModules.add(outdatedModuleId); } @@ -627,35 +716,44 @@ function computeOutdatedSelfAcceptedModules(outdatedModules) { } /** - * @param {ChunkPath} chunkPath - * @param {Iterable} outdatedModules - * @param {Iterable} deletedModules + * 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, + * which we can only compute *after* taking care of added and moved modules. + * + * @param {Map>} chunksAddedModules + * @param {Map>} chunksDeletedModules + * @returns {{ disposedModules: Set }} */ -function disposePhase(chunkPath, outdatedModules, deletedModules) { - for (const moduleId of outdatedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; +function updateChunksPhase(chunksAddedModules, chunksDeletedModules) { + for (const [chunkPath, addedModuleIds] of chunksAddedModules) { + for (const moduleId of addedModuleIds) { + addModuleToChunk(moduleId, chunkPath); } - - const data = disposeModule(module); - - moduleHotData.set(moduleId, data); } - for (const moduleId of deletedModules) { - const module = moduleCache[moduleId]; - if (!module) { - continue; + const disposedModules = new Set(); + for (const [chunkPath, addedModuleIds] of chunksDeletedModules) { + for (const moduleId of addedModuleIds) { + if (removeModuleFromChunk(moduleId, chunkPath)) { + disposedModules.add(moduleId); + } } + } - const noRemainingChunks = removeModuleFromChunk(moduleId, chunkPath); + return { disposedModules }; +} - if (noRemainingChunks) { - disposeModule(module); +/** + * @param {Iterable} outdatedModules + * @param {Set} disposedModules + */ +function disposePhase(outdatedModules, disposedModules) { + for (const moduleId of outdatedModules) { + disposeModule(moduleId, "replace"); + } - moduleHotData.delete(moduleId); - } + for (const moduleId of disposedModules) { + disposeModule(moduleId, "clear"); } // TODO(alexkirsz) Dependencies: remove outdated dependency from module @@ -668,10 +766,15 @@ function disposePhase(chunkPath, outdatedModules, deletedModules) { * Returns the persistent hot data that should be kept for the next module * instance. * - * @param {Module} module - * @returns {{}} + * @param {ModuleId} moduleId + * @param {"clear" | "replace"} mode */ -function disposeModule(module) { +function disposeModule(moduleId, mode) { + const module = moduleCache[moduleId]; + if (!module) { + return; + } + const hotState = moduleHotState.get(module); const data = {}; @@ -705,24 +808,27 @@ function disposeModule(module) { } } - return data; + switch (mode) { + case "clear": + moduleHotData.delete(module.id); + break; + case "replace": + moduleHotData.set(module.id, data); + break; + default: + invariant(mode, (mode) => `invalid mode: ${mode}`); + } } /** * - * @param {ChunkPath} chunkPath * @param {{ moduleId: ModuleId, errorHandler: true | Function }[]} outdatedSelfAcceptedModules - * @param {Map} newModuleFactories + * @param {Map} newModuleFactories */ -function applyPhase( - chunkPath, - outdatedSelfAcceptedModules, - newModuleFactories -) { +function applyPhase(outdatedSelfAcceptedModules, newModuleFactories) { // Update module factories. for (const [moduleId, factory] of newModuleFactories.entries()) { moduleFactories[moduleId] = factory; - addModuleToChunk(moduleId, chunkPath); } // TODO(alexkirsz) Run new runtime entries here. @@ -745,22 +851,178 @@ function applyPhase( } } +/** + * Utility function to ensure all variants of an enum are handled. + * @param {never} never + * @param {(arg: any) => string} computeMessage + * @returns {never} + */ +function invariant(never, computeMessage) { + throw new Error(`Invariant: ${computeMessage(never)}`); +} + /** * - * @param {ChunkPath} chunkPath - * @param {EcmascriptChunkUpdate} update + * @param {ChunkPath} chunkListPath + * @param {PartialUpdate} update + */ +function applyUpdate(chunkListPath, update) { + switch (update.type) { + case "ChunkListUpdate": + applyChunkListUpdate(chunkListPath, update); + break; + default: + invariant(update, (update) => `Unknown update type: ${update.type}`); + } +} + +/** + * + * @param {ChunkPath} chunkListPath + * @param {ChunkListUpdate} update */ -function applyUpdate(chunkPath, update) { - const { outdatedModules, newModuleFactories } = - computeOutdatedModules(update); +function applyChunkListUpdate(chunkListPath, update) { + if (update.merged != null) { + for (const merged of update.merged) { + switch (merged.type) { + case "EcmascriptMergedUpdate": + applyEcmascriptMergedUpdate(chunkListPath, merged); + break; + default: + invariant(merged, (merged) => `Unknown merged type: ${merged.type}`); + } + } + } - const deletedModules = new Set(update.deleted); + if (update.chunks != null) { + for (const [chunkPath, chunkUpdate] of Object.entries(update.chunks)) { + switch (chunkUpdate.type) { + case "added": + BACKEND.loadChunk(chunkPath); + break; + case "total": + BACKEND.reloadChunk?.(chunkPath); + break; + case "deleted": + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk?.(chunkPath); + break; + case "partial": + invariant( + chunkUpdate.instruction, + (instruction) => + `Unknown partial instruction: ${JSON.stringify(instruction)}.` + ); + default: + invariant( + chunkUpdate, + (chunkUpdate) => `Unknown chunk update type: ${chunkUpdate.type}` + ); + } + } + } +} +/** + * @param {ChunkPath} chunkPath + * @param {EcmascriptMergedUpdate} update + */ +function applyEcmascriptMergedUpdate(chunkPath, update) { + const { entries = {}, chunks = {} } = update; + const { added, modified, deleted, chunksAdded, chunksDeleted } = + computeChangedModules(entries, chunks); + const { outdatedModules, newModuleFactories } = computeOutdatedModules( + added, + modified, + entries + ); const outdatedSelfAcceptedModules = computeOutdatedSelfAcceptedModules(outdatedModules); + const { disposedModules } = updateChunksPhase(chunksAdded, chunksDeleted); + disposePhase(outdatedModules, disposedModules); + applyPhase(outdatedSelfAcceptedModules, newModuleFactories); +} - disposePhase(chunkPath, outdatedModules, deletedModules); - applyPhase(chunkPath, outdatedSelfAcceptedModules, newModuleFactories); +/** + * @param {Record} entries + * @param {Record} updates + * @returns {{ + * added: Map, + * modified: Map, + * deleted: Set, + * chunksAdded: Map>, + * chunksDeleted: Map>, + * }} + */ +function computeChangedModules(entries, updates) { + const chunksAdded = new Map(); + const chunksDeleted = new Map(); + const added = new Map(); + const modified = new Map(); + const deleted = new Set(); + + for (const [chunkPath, mergedChunkUpdate] of Object.entries(updates)) { + 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, chunkPath]); + } + 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 }; } /** @@ -850,17 +1112,35 @@ function getAffectedModuleEffects(moduleId) { } /** - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath * @param {import('../types/protocol').ServerMessage} update */ -function handleApply(chunkPath, update) { +function handleApply(chunkListPath, update) { switch (update.type) { - case "partial": - applyUpdate(chunkPath, update.instruction); + case "partial": { + // This indicates that the update is can be applied to the current state of the application. + applyUpdate(chunkListPath, update.instruction); break; - case "restart": + } + case "restart": { + // This indicates that there is no way to apply the update to the + // current state of the application, and that the application must be + // restarted. BACKEND.restart(); break; + } + case "notFound": { + // This indicates that the chunk list no longer exists: either the dynamic import which created it was removed, + // 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)) { + BACKEND.restart(); + } else { + disposeChunkList(chunkListPath); + } + break; + } default: throw new Error(`Unknown update type: ${update.type}`); } @@ -963,10 +1243,20 @@ function addModuleToChunk(moduleId, chunkPath) { } else { moduleChunks.add(chunkPath); } + + let chunkModules = chunkModulesMap.get(chunkPath); + if (!chunkModules) { + chunkModules = new Set([moduleId]); + chunkModulesMap.set(chunkPath, chunkModules); + } else { + chunkModules.add(moduleId); + } } /** * Returns the first chunk that included a module. + * This is used by the Node.js backend, hence why it's marked as unused in this + * file. * * @type {GetFirstModuleChunk} */ @@ -991,18 +1281,82 @@ function removeModuleFromChunk(moduleId, chunkPath) { const moduleChunks = moduleChunksMap.get(moduleId); moduleChunks.delete(chunkPath); - if (moduleChunks.size > 0) { + const chunkModules = chunkModulesMap.get(chunkPath); + chunkModules.delete(moduleId); + + const noRemainingModules = chunkModules.size === 0; + if (noRemainingModules) { + chunkModulesMap.delete(chunkPath); + } + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + } + + return noRemainingChunks; +} + +/** + * Diposes of a chunk list and its corresponding exclusive chunks. + * + * @param {ChunkPath} chunkListPath + * @returns {boolean} Whether the chunk list was disposed of. + */ +function disposeChunkList(chunkListPath) { + const chunkPaths = chunkListChunksMap.get(chunkListPath); + if (chunkPaths == null) { return false; } + chunkListChunksMap.delete(chunkListPath); + + for (const chunkPath of chunkPaths) { + const chunkChunkLists = chunkChunkListsMap.get(chunkPath); + chunkChunkLists.delete(chunkListPath); + + if (chunkChunkLists.size === 0) { + chunkChunkListsMap.delete(chunkPath); + disposeChunk(chunkPath); + } + } - moduleChunksMap.delete(moduleId); return true; } /** - * Instantiates a runtime module. + * Disposes of a chunk and its corresponding exclusive modules. + * + * @param {ChunkPath} chunkPath + * @returns {boolean} Whether the chunk was disposed of. */ +function disposeChunk(chunkPath) { + // This should happen whether or not the chunk has any modules in it. For instance, + // CSS chunks have no modules in them, but they still need to be unloaded. + loadedChunks.delete(chunkPath); + BACKEND.unloadChunk(chunkPath); + + const chunkModules = chunkModulesMap.get(chunkPath); + if (chunkModules == null) { + return false; + } + chunkModules.delete(chunkPath); + + for (const moduleId of chunkModules) { + const moduleChunks = moduleChunksMap.get(moduleId); + moduleChunks.delete(chunkPath); + + const noRemainingChunks = moduleChunks.size === 0; + if (noRemainingChunks) { + moduleChunksMap.delete(moduleId); + disposeModule(moduleId, "clear"); + } + } + + return true; +} + /** + * Instantiates a runtime module. * * @param {ModuleId} moduleId * @returns {Module} @@ -1012,18 +1366,57 @@ function instantiateRuntimeModule(moduleId) { } /** - * Subscribes to chunk updates from the update server and applies them. + * Subscribes to chunk list updates from the update server and applies them. * - * @param {ChunkPath} chunkPath + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths */ -function subscribeToChunkUpdates(chunkPath) { - // This adds a chunk update listener once the handler code has been loaded +function registerChunkList(chunkListPath, chunkPaths) { globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS.push([ - chunkPath, - handleApply.bind(null, chunkPath), + chunkListPath, + handleApply.bind(null, chunkListPath), ]); + + // Adding chunks to chunk lists and vice versa. + const chunks = new Set(chunkPaths); + chunkListChunksMap.set(chunkListPath, chunks); + for (const chunkPath of chunks) { + let chunkChunkLists = chunkChunkListsMap.get(chunkPath); + if (!chunkChunkLists) { + chunkChunkLists = new Set([chunkListPath]); + chunkChunkListsMap.set(chunkPath, chunkChunkLists); + } else { + chunkChunkLists.add(chunkListPath); + } + } +} + +/** + * Registers a chunk list and marks it as a runtime chunk list. This is called + * by the runtime of evaluated chunks. + * + * @param {ChunkPath} chunkListPath + * @param {ChunkPath[]} chunkPaths + */ +function registerChunkListAndMarkAsRuntime(chunkListPath, chunkPaths) { + registerChunkList(chunkListPath, chunkPaths); + markChunkListAsRuntime(chunkListPath); +} + +/** + * Marks a chunk list as a runtime chunk list. There can be more than one + * runtime chunk list. For instance, integration tests can have multiple chunk + * groups loaded at runtime, each with its own chunk list. + * + * @param {ChunkPath} chunkListPath + */ +function markChunkListAsRuntime(chunkListPath) { + runtimeChunkLists.add(chunkListPath); } +/** + * @param {ChunkPath} chunkPath + */ function markChunkAsLoaded(chunkPath) { const chunkLoader = chunkLoaders.get(chunkPath); if (!chunkLoader) { @@ -1044,6 +1437,7 @@ const runtime = { modules: moduleFactories, cache: moduleCache, instantiateRuntimeModule, + registerChunkList: registerChunkListAndMarkAsRuntime, }; /** @@ -1051,7 +1445,6 @@ const runtime = { */ function registerChunk([chunkPath, chunkModules, ...run]) { markChunkAsLoaded(chunkPath); - subscribeToChunkUpdates(chunkPath); for (const [moduleId, moduleFactory] of Object.entries(chunkModules)) { if (!moduleFactories[moduleId]) { moduleFactories[moduleId] = moduleFactory;