diff --git a/.github/workflows/pack-ci.yml b/.github/workflows/pack-ci.yml index 55ff8216aa..eadf214bff 100644 --- a/.github/workflows/pack-ci.yml +++ b/.github/workflows/pack-ci.yml @@ -165,16 +165,44 @@ jobs: if: ${{ !matrix.settings.docker }} shell: bash + hmr_e2e: + name: utoopack-ci-hmr-e2e + if: (github.event_name == 'push' && contains(github.event.head_commit.message, '(pack)')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.title, '(pack)')) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Init git submodules + run: git submodule update --init --recursive --depth 1 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 22.14.0 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly-2026-05-15 + - name: Setup Utoo + uses: utooland/setup-utoo@v1 + - name: Install dependencies + run: utoo install + - name: Build @utoo/pack + run: npm run build:local --workspace @utoo/pack + - name: Install Playwright browser + run: npx playwright install --with-deps chromium + - name: Run HMR browser regression tests + run: npm run test:e2e:pack-hmr + success: name: utoopack-ci-success runs-on: ubuntu-latest if: always() needs: - test + - hmr_e2e steps: - name: Check success run: | - if [ "${{ needs.test.result }}" == "failure" ] || [ "${{ needs.test.result }}" == "cancelled" ]; then + if [ "${{ needs.test.result }}" == "failure" ] || [ "${{ needs.test.result }}" == "cancelled" ] || [ "${{ needs.hmr_e2e.result }}" == "failure" ] || [ "${{ needs.hmr_e2e.result }}" == "cancelled" ]; then echo "Tests failed or were cancelled" exit 1 else diff --git a/crates/pack-api/src/project.rs b/crates/pack-api/src/project.rs index 2b3cdf1b91..be71cdf5a2 100644 --- a/crates/pack-api/src/project.rs +++ b/crates/pack-api/src/project.rs @@ -1167,11 +1167,15 @@ impl Project { #[turbo_tasks::function] pub(super) async fn client_compile_time_info(&self) -> Result> { + let watch = self.watch.enable; + let hot = self.config.dev_server().await?.hot.unwrap_or_default(); Ok(get_client_compile_time_info( (*self.config.target().await?).clone(), client_define_env(*self.config, self.process_env).await?, self.config.mode(), self.config.provider_config(), + Vc::cell(watch), + Vc::cell(hot), )) } @@ -1190,12 +1194,18 @@ impl Project { pub(super) async fn compile_time_info_for_platform(&self) -> Result> { let target = (*self.config.target().await?).clone(); match &*self.config.platform().await? { - Platform::Web => Ok(get_client_compile_time_info( - target, - client_define_env(*self.config, self.process_env).await?, - self.config.mode(), - self.config.provider_config(), - )), + Platform::Web => { + let watch = self.watch.enable; + let hot = self.config.dev_server().await?.hot.unwrap_or_default(); + Ok(get_client_compile_time_info( + target, + client_define_env(*self.config, self.process_env).await?, + self.config.mode(), + self.config.provider_config(), + Vc::cell(watch), + Vc::cell(hot), + )) + } Platform::Node => Ok(get_server_compile_time_info( target, self.config.define_env(), @@ -1209,6 +1219,8 @@ impl Project { pub async fn client_chunking_context(self: Vc) -> Result>> { let mode = self.mode(); let config = self.config(); + let watch = self.await?.watch.enable; + let hot = config.dev_server().await?.hot.unwrap_or_default(); let source_maps = if *config.source_maps().await? { SourceMapsType::Full } else { @@ -1216,6 +1228,8 @@ impl Project { }; Ok(get_client_chunking_context(ClientChunkingContextOptions { mode, + watch: Vc::cell(watch), + hot: Vc::cell(hot), root_path: self.project_path().owned().await?, client_root: self.client_root().owned().await?, client_root_to_root_path: rcstr!("/ROOT"), diff --git a/crates/pack-core/js/src/hmr/websocket.ts b/crates/pack-core/js/src/hmr/websocket.ts index 04a40df85f..e14cdcdc7f 100644 --- a/crates/pack-core/js/src/hmr/websocket.ts +++ b/crates/pack-core/js/src/hmr/websocket.ts @@ -12,6 +12,9 @@ type WebSocketMessage = let source: WebSocket | null = null; let eventCallbacks: Array<(event: WebSocketMessage) => void> = []; +const INITIAL_RECONNECT_DELAY_MS = 500; +const MAX_RECONNECT_DELAY_MS = 5_000; + // Helper function to dispatch messages to all event callbacks function dispatchMessage(message: WebSocketMessage) { for (const eventCallback of eventCallbacks) { @@ -72,21 +75,89 @@ let serverSessionId: number | null = null; // This is not used by Next.js, but it is used by the standalone turbopack-cli export function connectHMR(options: HMROptions) { + let reconnectTimer: ReturnType | null = null; + let reconnectAttempts = 0; + let pageIsUnloading = false; + + function clearReconnectTimer() { + if (reconnectTimer !== null) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + } + + function closeSocket(socket: WebSocket) { + socket.onopen = null; + socket.onerror = null; + socket.onclose = null; + socket.onmessage = null; + + if (source === socket) { + source = null; + } + + if ( + socket.readyState === WebSocket.CONNECTING || + socket.readyState === WebSocket.OPEN + ) { + socket.close(); + } + } + + function scheduleReconnect() { + if (reconnectTimer !== null || pageIsUnloading || reloading) { + return; + } + + const delay = Math.min( + INITIAL_RECONNECT_DELAY_MS * 2 ** reconnectAttempts, + MAX_RECONNECT_DELAY_MS, + ); + reconnectAttempts += 1; + reconnectTimer = setTimeout(() => { + reconnectTimer = null; + init(); + }, delay); + } + function init() { - if (source) source.close(); + if (pageIsUnloading || reloading) { + return; + } + + if (source) { + closeSocket(source); + } console.log("[HMR] connecting..."); + let socket: WebSocket; + try { + socket = new WebSocket(`${getSocketUrl()}${options.path}`); + } catch (error) { + console.error("[HMR] Failed to create WebSocket:", error); + scheduleReconnect(); + return; + } + + source = socket; + function handleOnline() { - window.console.log("[HMR] connected"); + if (source !== socket || pageIsUnloading || reloading) { + closeSocket(socket); + return; + } - // Send the turbopack-connected message to trigger handleSocketConnected - const connected: WebSocketMessage = { type: "turbopack-connected" }; - dispatchMessage(connected); + reconnectAttempts = 0; + window.console.log("[HMR] connected"); + // Direct turbopack-dev-server does not send a separate connected frame. + // Utoo does, but the socket-open notification is sufficient to restore + // subscriptions in both cases. + dispatchMessage({ type: "turbopack-connected" }); } function handleMessage(event: MessageEvent) { - if (reloading) { + if (source !== socket || reloading) { return; } @@ -99,22 +170,21 @@ export function connectHMR(options: HMROptions) { serverSessionId !== null && serverSessionId !== msg.data.sessionId ) { - window.location.reload(); reloading = true; + window.location.reload(); return; } serverSessionId = msg.data.sessionId; - // Convert to turbopack format and trigger handleSocketConnected - const connected: WebSocketMessage = { type: "turbopack-connected" }; - dispatchMessage(connected); + // Socket open already restored subscriptions. This frame only carries + // the Utoo server session id used to detect server restarts. return; } if (msg.action === "reload") { - window.location.reload(); reloading = true; + window.location.reload(); return; } @@ -146,27 +216,40 @@ export function connectHMR(options: HMROptions) { } } - function handleDisconnect(event?: Event) { - if (event && event.target !== source) { + function handleDisconnect(event: Event) { + if (event.target !== socket || source !== socket) { return; } - if (source) { - source.onerror = null; - source.onclose = null; - source.close(); - source = null; - } - + closeSocket(socket); window.console.warn("[HMR] disconnected"); + scheduleReconnect(); } - source = new WebSocket(`${getSocketUrl()}${options.path}`); - source.onopen = handleOnline; - source.onerror = handleDisconnect; - source.onclose = handleDisconnect; - source.onmessage = handleMessage; + socket.onopen = handleOnline; + socket.onerror = handleDisconnect; + socket.onclose = handleDisconnect; + socket.onmessage = handleMessage; } + // `pagehide` is not fired when a beforeunload prompt is cancelled, so it is + // safe to use as the point where reconnects should stop. + window.addEventListener("pagehide", () => { + pageIsUnloading = true; + clearReconnectTimer(); + if (source) { + closeSocket(source); + } + }); + + // A page restored from the back-forward cache needs a fresh HMR transport. + window.addEventListener("pageshow", (event) => { + if (event.persisted) { + pageIsUnloading = false; + reconnectAttempts = 0; + init(); + } + }); + init(); } diff --git a/crates/pack-core/src/client/context.rs b/crates/pack-core/src/client/context.rs index 1f92f5c12b..83de6ed3a1 100644 --- a/crates/pack-core/src/client/context.rs +++ b/crates/pack-core/src/client/context.rs @@ -93,13 +93,14 @@ pub async fn get_client_compile_time_info( define_env: Vc, mode: Vc, provider_config: Vc, + watch: Vc, + hot: Vc, ) -> Result> { + let mode_ref = mode.await?; let mut define_env = (*define_env.await?).clone(); define_env.extend([( "process.env.NODE_ENV".into(), - serde_json::to_string(mode.await?.node_env()) - .unwrap() - .into(), + serde_json::to_string(mode_ref.node_env()).unwrap().into(), )]); let define_env = Vc::cell(define_env); let environment = BrowserEnvironment { @@ -117,6 +118,7 @@ pub async fn get_client_compile_time_info( ) .defines(defines(define_env).to_resolved().await?) .free_var_references(free_vars(define_env, provider_config).to_resolved().await?) + .hot_module_replacement_enabled(mode_ref.is_development() && *watch.await? && *hot.await?) .cell() .await } @@ -555,6 +557,8 @@ pub async fn get_client_resolve_options_context( #[derive(Clone, Debug, PartialEq, Eq, Hash, TraceRawVcs, Encode, Decode)] pub struct ClientChunkingContextOptions { pub mode: Vc, + pub watch: Vc, + pub hot: Vc, pub root_path: FileSystemPath, pub client_root: FileSystemPath, pub client_root_to_root_path: RcStr, @@ -580,6 +584,8 @@ pub async fn get_client_chunking_context( ) -> Result>> { let ClientChunkingContextOptions { mode, + watch, + hot, root_path, client_root, client_root_to_root_path, @@ -685,9 +691,12 @@ pub async fn get_client_chunking_context( if mode.is_development() { builder = builder - .hot_module_replacement() .source_map_source_type(SourceMapSourceType::AbsoluteFileUri) .dynamic_chunk_content_loading(true); + + if *watch.await? && *hot.await? { + builder = builder.hot_module_replacement().dynamic_hmr_chunk_lists(); + } } else { let split_chunks = &config.optimization().await?.split_chunks; let style_groups_algorithm = config.css_chunking_algorithm().owned().await?; diff --git a/crates/pack-napi/src/pack_api/project.rs b/crates/pack-napi/src/pack_api/project.rs index 32f6c4208a..a49fc58480 100644 --- a/crates/pack-napi/src/pack_api/project.rs +++ b/crates/pack-napi/src/pack_api/project.rs @@ -2,7 +2,10 @@ use std::{ borrow::Cow, io::Write, path::PathBuf, - sync::LazyLock, + sync::{ + Arc, LazyLock, + atomic::{AtomicBool, Ordering}, + }, thread::{self, JoinHandle}, time::{Duration, Instant}, }; @@ -53,7 +56,7 @@ use turbo_unix_path::get_relative_path_to; use turbopack_core::{ PROJECT_FILESYSTEM_NAME, SOURCE_URL_PROTOCOL, source_map::{SourceMap, Token}, - version::{PartialUpdate, TotalUpdate, Update}, + version::{PartialUpdate, TotalUpdate, Update, Version}, }; use turbopack_ecmascript_hmr_protocol::{ClientUpdateInstruction, ResourceIdentifier}; use turbopack_trace_utils::{ @@ -711,25 +714,41 @@ pub fn project_hmr_events( #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External, identifier: RcStr, func: JsFunction, + expected_version: Option, ) -> napi::Result> { let turbopack_ctx = project.turbopack_ctx.clone(); let project = project.container; let session = TransientInstance::new(()); + let check_expected_version = Arc::new(AtomicBool::new(expected_version.is_some())); + let check_expected_version_after_emit = check_expected_version.clone(); subscribe( turbopack_ctx, func, { let outer_identifier = identifier.clone(); let session = session.clone(); + let expected_version = expected_version.clone(); + let check_expected_version = check_expected_version.clone(); move || { let identifier: RcStr = outer_identifier.clone(); let session = session.clone(); + let expected_version = expected_version.clone(); + let check_expected_version = check_expected_version.clone(); async move { let project = project.project().to_resolved().await?; let state = project .hmr_version_state(identifier.clone(), session) .to_resolved() .await?; + let should_check_expected_version = + check_expected_version.load(Ordering::Acquire); + let version_mismatch = if should_check_expected_version + && let Some(expected_version) = expected_version + { + state.get().id().owned().await? != expected_version + } else { + false + }; let update_op = hmr_update_with_issues_operation(project, identifier.clone(), state); @@ -750,12 +769,17 @@ pub fn project_hmr_events( state.set(to.clone()).await?; } } - Ok((Some(update.clone()), issues.clone())) + Ok(( + Some(update.clone()), + issues.clone(), + version_mismatch, + should_check_expected_version, + )) } } }, move |ctx| { - let (update, issues) = ctx.value; + let (update, issues, version_mismatch, should_check_expected_version) = ctx.value; let napi_issues = issues .iter() @@ -771,7 +795,11 @@ pub fn project_hmr_events( headers: None, }; let update = match update.as_deref() { - None | Some(Update::Missing) | Some(Update::Total(_)) => { + Some(Update::Missing) => ClientUpdateInstruction::not_found(&identifier), + _ if version_mismatch => { + ClientUpdateInstruction::restart(&identifier, &update_issues) + } + None | Some(Update::Total(_)) => { ClientUpdateInstruction::restart(&identifier, &update_issues) } Some(Update::Partial(update)) => ClientUpdateInstruction::partial( @@ -782,8 +810,13 @@ pub fn project_hmr_events( Some(Update::None) => ClientUpdateInstruction::issues(&identifier, &update_issues), }; + let result = ctx.env.to_js_value(&update)?; + if should_check_expected_version { + check_expected_version_after_emit.store(false, Ordering::Release); + } + Ok(vec![TurbopackResult { - result: ctx.env.to_js_value(&update)?, + result, issues: napi_issues, }]) }, diff --git a/crates/pack-tests/tests/snapshot/basic/asset_module_filename_dev/output/input_index_83022b3b.js b/crates/pack-tests/tests/snapshot/basic/asset_module_filename_dev/output/input_index_83022b3b.js deleted file mode 100644 index 89de5ef6fd..0000000000 --- a/crates/pack-tests/tests/snapshot/basic/asset_module_filename_dev/output/input_index_83022b3b.js +++ /dev/null @@ -1,5 +0,0 @@ -(globalThis["TURBOPACK_CHUNK_LISTS"] || (globalThis["TURBOPACK_CHUNK_LISTS"] = [])).push({ - script: typeof document === "object" ? document.currentScript : undefined, - chunks: ["input_0e1f1939.js"], - source: "entry" -}); \ No newline at end of file diff --git a/crates/pack-tests/tests/snapshot/basic/js_filename_dev/output/chunks/input_index_83022b3b.dev.js b/crates/pack-tests/tests/snapshot/basic/js_filename_dev/output/chunks/input_index_83022b3b.dev.js deleted file mode 100644 index af632e75f2..0000000000 --- a/crates/pack-tests/tests/snapshot/basic/js_filename_dev/output/chunks/input_index_83022b3b.dev.js +++ /dev/null @@ -1,5 +0,0 @@ -(globalThis["TURBOPACK_CHUNK_LISTS"] || (globalThis["TURBOPACK_CHUNK_LISTS"] = [])).push({ - script: typeof document === "object" ? document.currentScript : undefined, - chunks: ["chunks/input_message_7d12845b.dev.js","chunks/input_message_6ccb1ced.dev.js","chunks/input_index_c388c1fc.dev.js"], - source: "entry" -}); \ No newline at end of file diff --git a/crates/pack-tests/tests/snapshot/basic/js_filename_dev/output/chunks/input_message_6ccb1ced.dev.js b/crates/pack-tests/tests/snapshot/basic/js_filename_dev/output/chunks/input_message_6ccb1ced.dev.js index b067019973..698492c3c7 100644 --- a/crates/pack-tests/tests/snapshot/basic/js_filename_dev/output/chunks/input_message_6ccb1ced.dev.js +++ b/crates/pack-tests/tests/snapshot/basic/js_filename_dev/output/chunks/input_message_6ccb1ced.dev.js @@ -9,4 +9,4 @@ __turbopack_context__.v((parentImport) => { }); }); }), -]); \ No newline at end of file +]); diff --git a/crates/pack-tests/tests/snapshot/externals/basic/output/main.js b/crates/pack-tests/tests/snapshot/externals/basic/output/main.js index 529efc903f..e577587e6a 100644 --- a/crates/pack-tests/tests/snapshot/externals/basic/output/main.js +++ b/crates/pack-tests/tests/snapshot/externals/basic/output/main.js @@ -1038,6 +1038,10 @@ let BACKEND; let chunkUrl = getUrlFromScript(chunkPath); const resolver = getOrCreateResolver(chunkUrl); resolver.resolve(); + const exactChunkUrl = getUrlFromScript(chunk); + if (exactChunkUrl !== chunkUrl && /[?&]hmr=/.test(exactChunkUrl)) { + getOrCreateResolver(exactChunkUrl).resolve(); + } if (params == null) { return; } diff --git a/crates/pack-tests/tests/snapshot/externals/basic/output/main.js.map b/crates/pack-tests/tests/snapshot/externals/basic/output/main.js.map index bc35dd990c..4b117e2c68 100644 --- a/crates/pack-tests/tests/snapshot/externals/basic/output/main.js.map +++ b/crates/pack-tests/tests/snapshot/externals/basic/output/main.js.map @@ -7,5 +7,5 @@ {"offset": {"line": 640, "column": 0}, "map": {"version":3,"sources":["turbopack:///[turbopack]/browser/runtime/base/runtime-base.ts"],"sourcesContent":["/**\n * This file contains runtime types and functions that are shared between all\n * Turbopack *browser* ECMAScript runtimes.\n *\n * It will be appended to the runtime code of each runtime right after the\n * shared runtime utils.\n */\n\n/* eslint-disable @typescript-eslint/no-unused-vars */\n\n/// \n/// \n\n// Used in WebWorkers to tell the runtime about the chunk suffix\ndeclare var TURBOPACK_ASSET_SUFFIX: string\n// Used in WebWorkers to tell the runtime about the current chunk url since it\n// can't be detected via `document.currentScript`. Note it's stored in reversed\n// order to use `push` and `pop`\ndeclare var TURBOPACK_NEXT_CHUNK_URLS: ChunkUrl[] | undefined\n\n// Injected by rust code\ndeclare var CHUNK_BASE_PATH: string\ndeclare var ASSET_SUFFIX: string\ndeclare var CROSS_ORIGIN: 'anonymous' | 'use-credentials' | null\ndeclare var CHUNK_LOAD_RETRY_MAX_ATTEMPTS: number\ndeclare var CHUNK_LOAD_RETRY_BASE_DELAY_MS: number\ndeclare var CHUNK_LOAD_RETRY_MAX_JITTER_MS: number\n\n// Support runtime public path modes.\nfunction getRuntimeChunkBasePath(basePath: string = CHUNK_BASE_PATH): string {\n if (basePath === '__RUNTIME_PUBLIC_PATH__') {\n return contextPrototype.p()\n }\n if (basePath === '__AUTO_PUBLIC_PATH__') {\n return contextPrototype.p('auto')\n }\n return basePath\n}\n\ninterface TurbopackBrowserBaseContext extends TurbopackBaseContext {\n R: ResolvePathFromModule\n}\n\nconst browserContextPrototype =\n Context.prototype as TurbopackBrowserBaseContext\n\n// Provided by build or dev base\ndeclare function instantiateModule(\n id: ModuleId,\n sourceType: SourceType,\n sourceData: SourceData\n): Module\n\ntype RuntimeParams = {\n otherChunks: ChunkData[]\n runtimeModuleIds: ModuleId[]\n}\n\ntype ChunkRegistrationChunk =\n | ChunkPath\n | { getAttribute: (name: string) => string | null }\n | undefined\n\ntype ChunkRegistration = [\n chunkPath: ChunkRegistrationChunk,\n ...([RuntimeParams] | CompressedModuleFactories),\n]\n\ntype ChunkList = {\n script: ChunkRegistrationChunk\n chunks: ChunkData[]\n source: 'entry' | 'dynamic'\n}\n\ninterface RuntimeBackend {\n registerChunk: (\n chunkPath: ChunkPath | ChunkScript,\n params?: RuntimeParams\n ) => void\n /**\n * Returns the same Promise for the same chunk URL.\n */\n loadChunkCached: (sourceType: SourceType, chunkUrl: ChunkUrl) => Promise\n}\n\ninterface DevRuntimeBackend {\n reloadChunk?: (chunkUrl: ChunkUrl) => Promise\n unloadChunk?: (chunkUrl: ChunkUrl) => void\n restart: () => void\n}\n\nconst moduleFactories: ModuleFactories = new Map()\ncontextPrototype.M = moduleFactories\n\nconst availableModules: Map | true> = new Map()\n\nconst availableModuleChunks: Map | true> = new Map()\n\nfunction loadChunk(\n this: TurbopackBrowserBaseContext,\n chunkData: ChunkData\n): Promise {\n return loadChunkInternal(SourceType.Parent, this.m.id, chunkData)\n}\nbrowserContextPrototype.l = loadChunk\n\nfunction loadInitialChunk(chunkPath: ChunkPath, chunkData: ChunkData) {\n return loadChunkInternal(SourceType.Runtime, chunkPath, chunkData)\n}\n\nasync function loadChunkInternal(\n sourceType: SourceType,\n sourceData: SourceData,\n chunkData: ChunkData\n): Promise {\n if (typeof chunkData === 'string') {\n return loadChunkPath(sourceType, sourceData, chunkData)\n }\n\n const includedList = chunkData.included || []\n const modulesPromises = includedList.map((included) => {\n if (moduleFactories.has(included)) return true\n return availableModules.get(included)\n })\n if (modulesPromises.length > 0 && modulesPromises.every((p) => p)) {\n // When all included items are already loaded or loading, we can skip loading ourselves\n await Promise.all(modulesPromises)\n return\n }\n\n const includedModuleChunksList = chunkData.moduleChunks || []\n const moduleChunksPromises = includedModuleChunksList\n .map((included) => {\n // TODO(alexkirsz) Do we need this check?\n // if (moduleFactories[included]) return true;\n return availableModuleChunks.get(included)\n })\n .filter((p) => p)\n\n let promise: Promise\n if (moduleChunksPromises.length > 0) {\n // Some module chunks are already loaded or loading.\n\n if (moduleChunksPromises.length === includedModuleChunksList.length) {\n // When all included module chunks are already loaded or loading, we can skip loading ourselves\n await Promise.all(moduleChunksPromises)\n return\n }\n\n const moduleChunksToLoad: Set = new Set()\n for (const moduleChunk of includedModuleChunksList) {\n if (!availableModuleChunks.has(moduleChunk)) {\n moduleChunksToLoad.add(moduleChunk)\n }\n }\n\n for (const moduleChunkToLoad of moduleChunksToLoad) {\n const promise = loadChunkPath(sourceType, sourceData, moduleChunkToLoad)\n\n availableModuleChunks.set(moduleChunkToLoad, promise)\n\n moduleChunksPromises.push(promise)\n }\n\n promise = Promise.all(moduleChunksPromises)\n } else {\n promise = loadChunkPath(sourceType, sourceData, chunkData.path)\n\n // Mark all included module chunks as loading if they are not already loaded or loading.\n for (const includedModuleChunk of includedModuleChunksList) {\n if (!availableModuleChunks.has(includedModuleChunk)) {\n availableModuleChunks.set(includedModuleChunk, promise)\n }\n }\n }\n\n for (const included of includedList) {\n if (!availableModules.has(included)) {\n // It might be better to race old and new promises, but it's rare that the new promise will be faster than a request started earlier.\n // In production it's even more rare, because the chunk optimization tries to deduplicate modules anyway.\n availableModules.set(included, promise)\n }\n }\n\n await promise\n}\n\nconst loadedChunk = Promise.resolve(undefined)\nconst instrumentedBackendLoadChunks = new WeakMap<\n Promise,\n Promise | typeof loadedChunk\n>()\n// Do not make this async. React relies on referential equality of the returned Promise.\nfunction loadChunkByUrl(\n this: TurbopackBrowserBaseContext,\n chunkUrl: ChunkUrl\n) {\n return loadChunkByUrlInternal(SourceType.Parent, this.m.id, chunkUrl)\n}\nbrowserContextPrototype.L = loadChunkByUrl\n\nconst loadedScripts = new Map>()\n\n/**\n * Load an external script by creating a