Skip to main content

turbopack_ecmascript_runtime/
browser_runtime.rs

1use std::io::Write;
2
3use anyhow::Result;
4use indoc::writedoc;
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{ResolvedVc, Vc};
7use turbopack_core::{
8    chunk::{AssetSuffix, ChunkLoadRetry, CrossOrigin},
9    code_builder::{Code, CodeBuilder},
10    context::AssetContext,
11    environment::ChunkLoading,
12};
13use turbopack_ecmascript::utils::StringifyJs;
14
15use crate::{RuntimeType, embed_js::embed_static_code};
16
17pub fn chunk_update_listeners_global_name(chunk_loading_global: &str) -> String {
18    format!("{chunk_loading_global}_CHUNK_UPDATE_LISTENERS")
19}
20
21#[cfg(test)]
22mod tests {
23    use super::chunk_update_listeners_global_name;
24
25    #[test]
26    fn scopes_chunk_update_listeners_to_chunk_loading_global() {
27        assert_eq!(
28            chunk_update_listeners_global_name("TURBOPACK_APP"),
29            "TURBOPACK_APP_CHUNK_UPDATE_LISTENERS"
30        );
31    }
32}
33
34/// Returns the code for the ECMAScript runtime.
35#[turbo_tasks::function]
36pub async fn get_browser_runtime_code(
37    asset_context: ResolvedVc<Box<dyn AssetContext>>,
38    chunk_base_path: Vc<Option<RcStr>>,
39    asset_suffix: Vc<AssetSuffix>,
40    runtime_type: RuntimeType,
41    output_root_to_root_path: RcStr,
42    generate_source_map: bool,
43    chunk_loading_global: Vc<RcStr>,
44    cross_origin: Vc<CrossOrigin>,
45    chunk_load_retry: Vc<ChunkLoadRetry>,
46    include_async_module_runtime: bool,
47    chunk_loading: Vc<ChunkLoading>,
48    support_component_chunks: bool,
49) -> Result<Vc<Code>> {
50    let asset_context = *asset_context;
51    let environment = asset_context.compile_time_info().environment();
52
53    let shared_runtime_utils_code = embed_static_code(
54        asset_context,
55        rcstr!("shared/runtime/runtime-utils.ts"),
56        generate_source_map,
57    );
58
59    let mut runtime_base_code = vec!["browser/runtime/base/runtime-base.ts"];
60    match runtime_type {
61        RuntimeType::Production => runtime_base_code.push("browser/runtime/base/build-base.ts"),
62        RuntimeType::Development => {
63            runtime_base_code.push("shared/runtime/hmr-runtime.ts");
64            runtime_base_code.push("browser/runtime/base/dev-base.ts");
65        }
66        #[cfg(feature = "test")]
67        RuntimeType::Dummy => {
68            panic!("This configuration is not supported in the browser runtime")
69        }
70    }
71
72    let chunk_loading = &*chunk_loading.await?;
73
74    let mut runtime_backend_code = vec![];
75    match (chunk_loading, runtime_type) {
76        // The self-contained backend performs no runtime chunk loading and registers chunks only
77        // via `globalThis`/`self` (no DOM).
78        (ChunkLoading::Edge | ChunkLoading::SingleChunk, RuntimeType::Development) => {
79            runtime_backend_code
80                .push("browser/runtime/self-contained/runtime-backend-self-contained.ts");
81            runtime_backend_code
82                .push("browser/runtime/self-contained/dev-backend-self-contained.ts");
83        }
84        (ChunkLoading::Edge | ChunkLoading::SingleChunk, RuntimeType::Production) => {
85            runtime_backend_code
86                .push("browser/runtime/self-contained/runtime-backend-self-contained.ts");
87        }
88        // This case should never be hit.
89        (ChunkLoading::NodeJs, _) => {
90            panic!("Node.js runtime is not supported in the browser runtime!")
91        }
92        (ChunkLoading::Dom, RuntimeType::Development) => {
93            runtime_backend_code.push("browser/runtime/dom/runtime-backend-dom.ts");
94            runtime_backend_code.push("browser/runtime/dom/dev-backend-dom.ts");
95        }
96        (ChunkLoading::Dom, RuntimeType::Production) => {
97            runtime_backend_code.push("browser/runtime/dom/runtime-backend-dom.ts");
98        }
99
100        #[cfg(feature = "test")]
101        (_, RuntimeType::Dummy) => {
102            panic!("This configuration is not supported in the browser runtime")
103        }
104    };
105
106    let mut code: CodeBuilder = CodeBuilder::default();
107    let relative_root_path = output_root_to_root_path;
108    let chunk_base_path = chunk_base_path.await?;
109    let chunk_base_path = chunk_base_path.as_ref().map_or_else(|| "", |f| f.as_str());
110    let asset_suffix = asset_suffix.await?;
111    let chunk_loading_global = chunk_loading_global.await?;
112    let cross_origin = *cross_origin.await?;
113    let chunk_lists_global = format!("{}_CHUNK_LISTS", chunk_loading_global);
114    let chunk_update_listeners_global =
115        chunk_update_listeners_global_name(chunk_loading_global.as_str());
116
117    if *environment
118        .runtime_versions()
119        .supports_arrow_functions()
120        .await?
121    {
122        code += "(() => {\n";
123    } else {
124        code += "(function(){\n";
125    }
126
127    // A shared runtime can execute before any async chunk has initialized the chunk queue.
128    // Treat a missing queue as empty, but return when another runtime has already installed its
129    // registry object.
130    writedoc!(
131        code,
132        r#"
133            var chunksToRegister = globalThis[{}];
134            if (chunksToRegister === undefined) {{
135                chunksToRegister = [];
136            }} else if (!Array.isArray(chunksToRegister)) {{
137                return;
138            }}
139
140            var CHUNK_BASE_PATH = {};
141            var RELATIVE_ROOT_PATH = {};
142            var RUNTIME_PUBLIC_PATH = {};
143            const SUPPORT_COMPONENT_CHUNKS = {};
144        "#,
145        StringifyJs(&chunk_loading_global),
146        StringifyJs(chunk_base_path),
147        StringifyJs(relative_root_path.as_str()),
148        StringifyJs(chunk_base_path),
149        support_component_chunks,
150    )?;
151
152    if matches!(runtime_type, RuntimeType::Development) {
153        writedoc!(
154            code,
155            r#"
156                globalThis[{chunk_update_listeners_global}] ||= [];
157                var CHUNK_UPDATE_LISTENERS = {{
158                    push: (registration) => globalThis[{chunk_update_listeners_global}].push(registration),
159                }};
160            "#,
161            chunk_update_listeners_global = StringifyJs(&chunk_update_listeners_global),
162        )?;
163    }
164
165    match &*asset_suffix {
166        AssetSuffix::None => {
167            writedoc!(
168                code,
169                r#"
170                    var ASSET_SUFFIX = "";
171                "#
172            )?;
173        }
174        AssetSuffix::Constant(suffix) => {
175            writedoc!(
176                code,
177                r#"
178                    var ASSET_SUFFIX = {};
179                "#,
180                StringifyJs(suffix.as_str())
181            )?;
182        }
183        AssetSuffix::Inferred => {
184            if matches!(
185                chunk_loading,
186                ChunkLoading::Edge | ChunkLoading::SingleChunk
187            ) {
188                panic!("AssetSuffix::Inferred is not supported in Edge or single-chunk runtimes");
189            }
190            writedoc!(
191                code,
192                r#"
193                    var ASSET_SUFFIX = getAssetSuffixFromScriptSrc();
194                "#
195            )?;
196        }
197        AssetSuffix::FromGlobal(global_name) => {
198            writedoc!(
199                code,
200                r#"
201                    var ASSET_SUFFIX = globalThis[{}] || "";
202                "#,
203                StringifyJs(global_name)
204            )?;
205        }
206    }
207
208    let cross_origin = cross_origin.as_str();
209    writedoc!(
210        code,
211        r#"
212            var CROSS_ORIGIN = {};
213        "#,
214        StringifyJs(&cross_origin)
215    )?;
216
217    // The chunk-load retry policy is owned by the framework (e.g. Next.js) and
218    // passed in via the chunking context, so the runtime never hard-codes it.
219    let chunk_load_retry = *chunk_load_retry.await?;
220    writedoc!(
221        code,
222        r#"
223            var CHUNK_LOAD_RETRY_MAX_ATTEMPTS = {};
224            var CHUNK_LOAD_RETRY_BASE_DELAY_MS = {};
225            var CHUNK_LOAD_RETRY_MAX_JITTER_MS = {};
226        "#,
227        chunk_load_retry.max_retry_attempts,
228        chunk_load_retry.base_delay_ms,
229        chunk_load_retry.max_jitter_ms,
230    )?;
231
232    code.push_code(&*shared_runtime_utils_code.await?);
233    if include_async_module_runtime {
234        code.push_code(
235            &*embed_static_code(
236                asset_context,
237                rcstr!("shared/runtime/async-module.ts"),
238                generate_source_map,
239            )
240            .await?,
241        );
242    }
243    for runtime_code in runtime_base_code {
244        code.push_code(
245            &*embed_static_code(asset_context, runtime_code.into(), generate_source_map).await?,
246        );
247    }
248
249    if *environment.supports_commonjs_externals().await? {
250        code.push_code(
251            &*embed_static_code(
252                asset_context,
253                rcstr!("shared-node/base-externals-utils.ts"),
254                generate_source_map,
255            )
256            .await?,
257        );
258    }
259    if *environment.node_externals().await? {
260        code.push_code(
261            &*embed_static_code(
262                asset_context,
263                rcstr!("shared-node/node-externals-utils.ts"),
264                generate_source_map,
265            )
266            .await?,
267        );
268    }
269    for backend_code in runtime_backend_code {
270        code.push_code(
271            &*embed_static_code(asset_context, backend_code.into(), generate_source_map).await?,
272        );
273    }
274
275    // Registering chunks/chunk lists depends on the BACKEND variable set by the specific
276    // runtime code, so it must be appended after it. `registerChunk` handles both queued forms:
277    // chunk-registration arrays and inlined entry-only params objects.
278    writedoc!(
279        code,
280        r#"
281            globalThis[{chunk_loading_global}] = {{ push: registerChunk }};
282            chunksToRegister.forEach(registerChunk);
283        "#,
284        chunk_loading_global = StringifyJs(&chunk_loading_global),
285    )?;
286    if matches!(runtime_type, RuntimeType::Development) {
287        writedoc!(
288            code,
289            r#"
290            var chunkListsToRegister = globalThis[{chunk_lists_global}] || [];
291            globalThis[{chunk_lists_global}] = {{ push: registerChunkList }};
292            chunkListsToRegister.forEach(registerChunkList);
293        "#,
294            chunk_lists_global = StringifyJs(&chunk_lists_global),
295        )?;
296    }
297    writedoc!(
298        code,
299        r#"
300            }})();
301        "#
302    )?;
303
304    Ok(Code::cell(code.build()))
305}
306
307/// Returns the code for the ECMAScript worker entrypoint bootstrap.
308pub fn get_worker_runtime_code(
309    asset_context: Vc<Box<dyn AssetContext>>,
310    generate_source_map: bool,
311) -> Result<Vc<Code>> {
312    Ok(embed_static_code(
313        asset_context,
314        rcstr!("browser/runtime/base/worker-entrypoint.ts"),
315        generate_source_map,
316    ))
317}