turbopack_browser/ecmascript/
worker.rs1use std::io::Write;
2
3use anyhow::Result;
4use indoc::writedoc;
5use turbo_rcstr::{RcStr, rcstr};
6use turbo_tasks::{ResolvedVc, ValueToString, Vc};
7use turbo_tasks_fs::{File, FileContent, FileSystemPath};
8use turbo_tasks_hash::hash_xxh3_hash64;
9use turbopack_core::{
10 asset::{Asset, AssetContent},
11 chunk::{ChunkingContext, MinifyType},
12 code_builder::{Code, CodeBuilder},
13 ident::AssetIdent,
14 output::{OutputAsset, OutputAssetsReference, OutputAssetsWithReferenced},
15 source_map::{GenerateSourceMap, SourceMapAsset},
16};
17use turbopack_ecmascript::minify::minify;
18
19use crate::BrowserChunkingContext;
20
21#[turbo_tasks::value(shared)]
26#[derive(ValueToString)]
27#[value_to_string("Ecmascript Browser Worker Entrypoint")]
28pub struct EcmascriptBrowserWorkerEntrypoint {
29 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
30 forwarded_globals: ResolvedVc<Vec<RcStr>>,
34}
35
36#[turbo_tasks::value_impl]
37impl EcmascriptBrowserWorkerEntrypoint {
38 #[turbo_tasks::function]
39 pub async fn new(
40 chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
41 forwarded_globals: Vc<Vec<RcStr>>,
42 ) -> Result<Vc<Self>> {
43 Ok(EcmascriptBrowserWorkerEntrypoint {
44 chunking_context,
45 forwarded_globals: forwarded_globals.to_resolved().await?,
46 }
47 .cell())
48 }
49
50 #[turbo_tasks::function]
51 async fn code(self: Vc<Self>) -> Result<Vc<Code>> {
52 let this = self.await?;
53
54 let source_maps = *this
55 .chunking_context
56 .reference_chunk_source_maps(Vc::upcast(self))
57 .await?;
58
59 let forwarded_globals = this.forwarded_globals.await?;
60 let shared_runtime =
63 match ResolvedVc::try_downcast_type::<BrowserChunkingContext>(this.chunking_context) {
64 Some(browser_chunking_context) => {
65 *browser_chunking_context.shared_runtime().await?
66 }
67 None => false,
68 };
69 let mut code = generate_worker_bootstrap_code(&forwarded_globals, shared_runtime)?;
70
71 if let MinifyType::Minify { mangle } = *this.chunking_context.minify_type().await? {
72 code = minify(code, source_maps, mangle)?;
73 }
74
75 Ok(code.cell())
76 }
77
78 #[turbo_tasks::function]
79 async fn ident_for_path(&self) -> Result<Vc<AssetIdent>> {
80 let chunk_root_path = self.chunking_context.chunk_root_path().owned().await?;
81 let forwarded_globals = self.forwarded_globals.await?;
82 let globals_hash = hash_xxh3_hash64(&*forwarded_globals);
83 let ident = AssetIdent::from_path(chunk_root_path)
84 .with_modifier(rcstr!("turbopack worker entrypoint"))
85 .with_modifier(format!("{globals_hash:08x}").into());
86 Ok(ident.into_vc())
87 }
88
89 #[turbo_tasks::function]
90 async fn source_map(self: Vc<Self>) -> Result<Vc<SourceMapAsset>> {
91 let this = self.await?;
92 Ok(SourceMapAsset::new(
93 *this.chunking_context,
94 self.ident_for_path(),
95 Vc::upcast(self),
96 ))
97 }
98}
99
100#[turbo_tasks::value_impl]
101impl OutputAssetsReference for EcmascriptBrowserWorkerEntrypoint {
102 #[turbo_tasks::function]
103 async fn references(self: Vc<Self>) -> Result<Vc<OutputAssetsWithReferenced>> {
104 Ok(OutputAssetsWithReferenced::from_assets(Vc::cell(vec![
105 ResolvedVc::upcast(self.source_map().to_resolved().await?),
106 ])))
107 }
108}
109
110#[turbo_tasks::value_impl]
111impl OutputAsset for EcmascriptBrowserWorkerEntrypoint {
112 #[turbo_tasks::function]
113 async fn path(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
114 let this = self.await?;
115 let ident = self.ident_for_path();
116 Ok(this.chunking_context.chunk_path(
117 Some(Vc::upcast(self)),
118 ident,
119 Some(rcstr!("turbopack-worker")),
120 rcstr!(".js"),
121 ))
122 }
123}
124
125#[turbo_tasks::value_impl]
126impl Asset for EcmascriptBrowserWorkerEntrypoint {
127 #[turbo_tasks::function]
128 async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
129 Ok(AssetContent::file(
130 FileContent::Content(File::from(
131 self.code()
132 .to_rope_with_magic_comments(|| self.source_map())
133 .await?,
134 ))
135 .cell(),
136 ))
137 }
138}
139
140#[turbo_tasks::value_impl]
141impl GenerateSourceMap for EcmascriptBrowserWorkerEntrypoint {
142 #[turbo_tasks::function]
143 fn generate_source_map(self: Vc<Self>) -> Vc<FileContent> {
144 self.code().generate_source_map()
145 }
146}
147
148fn generate_worker_bootstrap_code(
153 forwarded_globals: &[RcStr],
154 shared_runtime: bool,
155) -> Result<Code> {
156 let mut code: CodeBuilder = CodeBuilder::default();
157
158 let mut global_assignments = vec![
162 "TURBOPACK_NEXT_CHUNK_URLS: chunkUrls".to_string(),
163 "TURBOPACK_ASSET_SUFFIX: param(1)".to_string(),
164 "TURBOPACK_CHUNK_BASE_PATH: param(2)".to_string(),
165 ];
166 for (i, name) in forwarded_globals.iter().enumerate() {
167 global_assignments.push(format!("{name}: param({n})", n = i + 3));
169 }
170 let globals_js = global_assignments.join(",\n ");
171
172 writedoc!(
187 code,
188 r##"
189 (function() {{
190 function abort(message) {{
191 console.error(message);
192 throw new Error(message);
193 }}
194 if (
195 typeof self["WorkerGlobalScope"] === "undefined" ||
196 !(self instanceof self["WorkerGlobalScope"])
197 ) {{
198 abort("Worker entrypoint must be loaded in a worker context");
199 }}
200
201 // Try querystring first (SharedWorker), then hash (regular Worker)
202 var url = new URL(location.href);
203 var paramsString = url.searchParams.get("params");
204 if (!paramsString && url.hash.startsWith("#params=")) {{
205 paramsString = decodeURIComponent(url.hash.slice("#params=".length));
206 }}
207
208 if (!paramsString) abort("Missing worker bootstrap config");
209
210 var params = JSON.parse(paramsString);
211 var param = (n) => typeof params[n] === 'string' ? params[n] : '';
212 var chunkUrls = Array.isArray(params[0]) ? params[0] : [];
213 "##,
214 )?;
215
216 if shared_runtime {
217 writedoc!(
220 code,
221 r##"
222
223 // Chunks are relative to the origin; only allow loading same-origin scripts.
224 function sameOriginUrl(chunk) {{
225 var chunkUrl = new URL(chunk, location.origin);
226 if (chunkUrl.origin !== location.origin) {{
227 abort("Refusing to load script from foreign origin: " + chunkUrl.origin);
228 }}
229 return chunkUrl.toString();
230 }}
231
232 // The Turbopack runtime is the last asset emitted by (see
233 // `BrowserChunkingContext::evaluated_chunk_group`). `createWorker`
234 // reverses the chunk list, so it is the first item in `chunkUrls`.
235 var runtimeUrl = chunkUrls.length > 0 ? chunkUrls.shift() : undefined;
236 "##,
237 )?;
238 }
239
240 writedoc!(
241 code,
242 r##"
243
244 Object.assign(self, {{
245 {0}
246 }});
247 "##,
248 globals_js
249 )?;
250
251 if shared_runtime {
252 writedoc!(
253 code,
254 r##"
255
256 if (chunkUrls.length > 0 || runtimeUrl) {{
257 var scriptsToLoad = [];
258 for (var i = 0; i < chunkUrls.length; i++) {{
259 scriptsToLoad.push(sameOriginUrl(chunkUrls[i]));
260 }}
261
262 // As scripts are loaded, allow them to pop from the array
263 chunkUrls.reverse();
264
265 // Load the runtime last so it drains the registrations enqueued above.
266 if (runtimeUrl) {{
267 scriptsToLoad.push(sameOriginUrl(runtimeUrl));
268 }}
269
270 importScripts.apply(self, scriptsToLoad);
271 }}
272 }})();
273 "##,
274 )?;
275 } else {
276 writedoc!(
277 code,
278 r##"
279
280 if (chunkUrls.length > 0) {{
281 var scriptsToLoad = [];
282 for (var i = 0; i < chunkUrls.length; i++) {{
283 var chunk = chunkUrls[i];
284 // Chunks are relative to the origin.
285 var chunkUrl = new URL(chunk, location.origin);
286 if (chunkUrl.origin !== location.origin) {{
287 abort("Refusing to load script from foreign origin: " + chunkUrl.origin);
288 }}
289 scriptsToLoad.push(chunkUrl.toString());
290 }}
291
292 // As scripts are loaded, allow them to pop from the array
293 chunkUrls.reverse();
294 importScripts.apply(self, scriptsToLoad);
295 }}
296 }})();
297 "##,
298 )?;
299 }
300
301 Ok(code.build())
302}