Skip to main content

turbopack_browser/ecmascript/
worker.rs

1use 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
19/// A pre-compiled worker entrypoint that bootstraps workers by reading config from URL params.
20///
21/// The worker receives a JSON array via URL params of the following structure:
22/// `[TURBOPACK_NEXT_CHUNK_URLS, ASSET_SUFFIX, ...forwarded_global_values]`
23#[turbo_tasks::value(shared)]
24#[derive(ValueToString)]
25#[value_to_string("Ecmascript Browser Worker Entrypoint")]
26pub struct EcmascriptBrowserWorkerEntrypoint {
27    chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
28    /// Global variable names to forward from main thread to worker.
29    /// These are assigned to `self` in the worker scope before loading chunks.
30    /// Values are passed via URL params at indices 2+.
31    forwarded_globals: ResolvedVc<Vec<RcStr>>,
32}
33
34#[turbo_tasks::value_impl]
35impl EcmascriptBrowserWorkerEntrypoint {
36    #[turbo_tasks::function]
37    pub async fn new(
38        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
39        forwarded_globals: Vc<Vec<RcStr>>,
40    ) -> Result<Vc<Self>> {
41        Ok(EcmascriptBrowserWorkerEntrypoint {
42            chunking_context,
43            forwarded_globals: forwarded_globals.to_resolved().await?,
44        }
45        .cell())
46    }
47
48    #[turbo_tasks::function]
49    async fn code(self: Vc<Self>) -> Result<Vc<Code>> {
50        let this = self.await?;
51
52        let source_maps = *this
53            .chunking_context
54            .reference_chunk_source_maps(Vc::upcast(self))
55            .await?;
56
57        let forwarded_globals = this.forwarded_globals.await?;
58        let mut code = generate_worker_bootstrap_code(&forwarded_globals)?;
59
60        if let MinifyType::Minify { mangle } = *this.chunking_context.minify_type().await? {
61            code = minify(code, source_maps, mangle)?;
62        }
63
64        Ok(code.cell())
65    }
66
67    #[turbo_tasks::function]
68    async fn ident_for_path(&self) -> Result<Vc<AssetIdent>> {
69        let chunk_root_path = self.chunking_context.chunk_root_path().owned().await?;
70        let forwarded_globals = self.forwarded_globals.await?;
71        let globals_hash = hash_xxh3_hash64(&*forwarded_globals);
72        let ident = AssetIdent::from_path(chunk_root_path)
73            .with_modifier(rcstr!("turbopack worker entrypoint"))
74            .with_modifier(format!("{globals_hash:08x}").into());
75        Ok(ident)
76    }
77
78    #[turbo_tasks::function]
79    async fn source_map(self: Vc<Self>) -> Result<Vc<SourceMapAsset>> {
80        let this = self.await?;
81        Ok(SourceMapAsset::new(
82            *this.chunking_context,
83            self.ident_for_path(),
84            Vc::upcast(self),
85        ))
86    }
87}
88
89#[turbo_tasks::value_impl]
90impl OutputAssetsReference for EcmascriptBrowserWorkerEntrypoint {
91    #[turbo_tasks::function]
92    async fn references(self: Vc<Self>) -> Result<Vc<OutputAssetsWithReferenced>> {
93        Ok(OutputAssetsWithReferenced::from_assets(Vc::cell(vec![
94            ResolvedVc::upcast(self.source_map().to_resolved().await?),
95        ])))
96    }
97}
98
99#[turbo_tasks::value_impl]
100impl OutputAsset for EcmascriptBrowserWorkerEntrypoint {
101    #[turbo_tasks::function]
102    async fn path(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
103        let this = self.await?;
104        let ident = self.ident_for_path();
105        Ok(this.chunking_context.chunk_path(
106            Some(Vc::upcast(self)),
107            ident,
108            Some(rcstr!("turbopack-worker")),
109            rcstr!(".js"),
110        ))
111    }
112}
113
114#[turbo_tasks::value_impl]
115impl Asset for EcmascriptBrowserWorkerEntrypoint {
116    #[turbo_tasks::function]
117    async fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
118        Ok(AssetContent::file(
119            FileContent::Content(File::from(
120                self.code()
121                    .to_rope_with_magic_comments(|| self.source_map())
122                    .await?,
123            ))
124            .cell(),
125        ))
126    }
127}
128
129#[turbo_tasks::value_impl]
130impl GenerateSourceMap for EcmascriptBrowserWorkerEntrypoint {
131    #[turbo_tasks::function]
132    fn generate_source_map(self: Vc<Self>) -> Vc<FileContent> {
133        self.code().generate_source_map()
134    }
135}
136
137/// Generates the worker bootstrap code as inline JavaScript.
138///
139/// The worker receives a JSON array via URL params of the following structure:
140/// `[TURBOPACK_NEXT_CHUNK_URLS, ASSET_SUFFIX, ...forwarded_global_values]`
141fn generate_worker_bootstrap_code(forwarded_globals: &[RcStr]) -> Result<Code> {
142    let mut code: CodeBuilder = CodeBuilder::default();
143
144    // Generate the Object.assign properties for forwarded globals
145    // params[0] = chunk URLs, params[1] = ASSET_SUFFIX, params[2+] = forwarded globals
146    let mut global_assignments = vec![
147        "TURBOPACK_NEXT_CHUNK_URLS: chunkUrls".to_string(),
148        "TURBOPACK_ASSET_SUFFIX: param(1)".to_string(),
149    ];
150    for (i, name) in forwarded_globals.iter().enumerate() {
151        // Forwarded globals start at params[2]
152        global_assignments.push(format!("{name}: param({n})", n = i + 2));
153    }
154    let globals_js = global_assignments.join(",\n    ");
155
156    // This code is slightly paranoid to avoid being useful as an XSS gadget.
157    //
158    // First, it verifies that it is running in a worker environment, which
159    // guarantees that the requestor shares the same origin as the script
160    // itself.
161    //
162    // Additionally, the code only allows loading scripts from the same origin,
163    // mitigating the risk that the worker could be exploited to fetch or run
164    // scripts from cross-origin sources.
165    //
166    // The snippet also validates types for all parameters to prevent unexpected
167    // usage.
168
169    writedoc!(
170        code,
171        r##"
172        (function() {{
173        function abort(message) {{
174            console.error(message);
175            throw new Error(message);
176        }}
177        if (
178            typeof self["WorkerGlobalScope"] === "undefined" ||
179            !(self instanceof self["WorkerGlobalScope"])
180        ) {{
181            abort("Worker entrypoint must be loaded in a worker context");
182        }}
183
184        // Try querystring first (SharedWorker), then hash (regular Worker)
185        var url = new URL(location.href);
186        var paramsString = url.searchParams.get("params");
187        if (!paramsString && url.hash.startsWith("#params=")) {{
188            paramsString = decodeURIComponent(url.hash.slice("#params=".length));
189        }}
190
191        if (!paramsString) abort("Missing worker bootstrap config");
192
193        var params = JSON.parse(paramsString);
194        var param = (n) => typeof params[n] === 'string' ? params[n] : '';
195        var chunkUrls = Array.isArray(params[0]) ? params[0] : [];
196
197        Object.assign(self, {{
198            {0}
199        }});
200
201        if (chunkUrls.length > 0) {{
202            var scriptsToLoad = [];
203            for (var i = 0; i < chunkUrls.length; i++) {{
204                var chunk = chunkUrls[i];
205                // Chunks are relative to the origin.
206                var chunkUrl = new URL(chunk, location.origin);
207                if (chunkUrl.origin !== location.origin) {{
208                    abort("Refusing to load script from foreign origin: " + chunkUrl.origin);
209                }}
210                scriptsToLoad.push(chunkUrl.toString());
211            }}
212
213            // As scripts are loaded, allow them to pop from the array
214            chunkUrls.reverse();
215            importScripts.apply(self, scriptsToLoad);
216        }}
217        }})();
218        "##,
219        globals_js
220    )?;
221
222    Ok(code.build())
223}