Skip to main content

turbopack_ecmascript/worker_chunk/
module.rs

1use anyhow::{Result, bail};
2use indoc::formatdoc;
3use turbo_rcstr::rcstr;
4use turbo_tasks::{ResolvedVc, TryJoinIterExt, ValueToString, Vc};
5use turbopack_core::{
6    chunk::{
7        AsyncModuleInfo, ChunkData, ChunkGroupType, ChunkableModule, ChunkingContext,
8        ChunkingContextExt, ChunkingType, ChunksData, EvaluatableAsset,
9        availability_info::AvailabilityInfo,
10    },
11    context::AssetContext,
12    ident::AssetIdent,
13    module::{Module, ModuleSideEffects},
14    module_graph::{ModuleGraph, chunk_group_info::ChunkGroup},
15    output::{OutputAsset, OutputAssets, OutputAssetsWithReferenced},
16    reference::{ModuleReference, ModuleReferences},
17    resolve::ModuleResolveResult,
18};
19
20use super::worker_type::WorkerType;
21use crate::{
22    chunk::{
23        EcmascriptChunkItemContent, EcmascriptChunkItemOptions, EcmascriptChunkPlaceable,
24        EcmascriptExports, data::EcmascriptChunkData, ecmascript_chunk_item,
25    },
26    runtime_functions::{TURBOPACK_CREATE_WORKER, TURBOPACK_EXPORT_VALUE},
27    utils::StringifyJs,
28};
29
30/// The WorkerLoaderModule is a module that creates a separate root chunk group for the given module
31/// and exports a URL (for web workers) or file path (for Node.js workers) to pass to the worker
32/// constructor.
33#[turbo_tasks::value]
34pub struct WorkerLoaderModule {
35    pub inner: ResolvedVc<Box<dyn ChunkableModule>>,
36    pub worker_type: WorkerType,
37    pub asset_context: ResolvedVc<Box<dyn AssetContext>>,
38}
39
40#[turbo_tasks::value_impl]
41impl WorkerLoaderModule {
42    #[turbo_tasks::function]
43    pub fn new(
44        module: ResolvedVc<Box<dyn ChunkableModule>>,
45        worker_type: WorkerType,
46        asset_context: ResolvedVc<Box<dyn AssetContext>>,
47    ) -> Vc<Self> {
48        Self::cell(WorkerLoaderModule {
49            inner: module,
50            worker_type,
51            asset_context,
52        })
53    }
54
55    #[turbo_tasks::function]
56    async fn chunk_group(
57        self: Vc<Self>,
58        chunking_context: Vc<Box<dyn ChunkingContext>>,
59        module_graph: Vc<ModuleGraph>,
60    ) -> Result<Vc<OutputAssetsWithReferenced>> {
61        let this = self.await?;
62        Ok(match this.worker_type {
63            WorkerType::WebWorker | WorkerType::SharedWebWorker => {
64                let ident = this
65                    .inner
66                    .ident()
67                    .owned()
68                    .await?
69                    .with_modifier(this.worker_type.chunk_modifier_str())
70                    .into_vc();
71                chunking_context.evaluated_chunk_group_assets(
72                    ident,
73                    ChunkGroup::Isolated(ResolvedVc::upcast(this.inner)),
74                    module_graph,
75                    AvailabilityInfo::root(),
76                )
77            }
78            // WorkerThreads are treated as an entry point, webworkers probably should too but
79            // currently it would lead to a cascade that we need to address.
80            WorkerType::NodeWorkerThread => {
81                let Some(evaluatable) =
82                    ResolvedVc::try_sidecast::<Box<dyn EvaluatableAsset>>(this.inner)
83                else {
84                    bail!("Worker module must be evaluatable");
85                };
86
87                let worker_path = chunking_context
88                    .chunk_path(
89                        None,
90                        this.inner.ident(),
91                        Some(rcstr!("[worker thread]")),
92                        rcstr!(".js"),
93                    )
94                    .owned()
95                    .await?;
96
97                let entry_result = chunking_context
98                    .root_entry_chunk_group(
99                        worker_path,
100                        ChunkGroup::Isolated(ResolvedVc::upcast(evaluatable)),
101                        module_graph,
102                        OutputAssets::empty(),
103                        OutputAssets::empty(),
104                    )
105                    .await?;
106
107                OutputAssetsWithReferenced {
108                    assets: ResolvedVc::cell(vec![entry_result.asset]),
109                    referenced_assets: ResolvedVc::cell(vec![]),
110                    references: ResolvedVc::cell(vec![]),
111                }
112                .cell()
113            }
114        })
115    }
116
117    #[turbo_tasks::function]
118    async fn chunks_data(
119        self: Vc<Self>,
120        chunking_context: Vc<Box<dyn ChunkingContext>>,
121        module_graph: Vc<ModuleGraph>,
122    ) -> Result<Vc<ChunksData>> {
123        Ok(ChunkData::from_assets(
124            chunking_context.output_root().owned().await?,
125            *self
126                .chunk_group(chunking_context, module_graph)
127                .await?
128                .assets,
129        ))
130    }
131
132    /// Returns output assets including the worker entrypoint for web workers.
133    #[turbo_tasks::function]
134    async fn chunk_group_with_type(
135        self: Vc<Self>,
136        chunking_context: Vc<Box<dyn ChunkingContext>>,
137        module_graph: Vc<ModuleGraph>,
138    ) -> Result<Vc<OutputAssetsWithReferenced>> {
139        let this = self.await?;
140        Ok(match this.worker_type {
141            WorkerType::WebWorker | WorkerType::SharedWebWorker => self
142                .chunk_group(chunking_context, module_graph)
143                .concatenate_asset(chunking_context.worker_entrypoint()),
144            WorkerType::NodeWorkerThread => {
145                // Node.js workers don't need a separate entrypoint asset
146                self.chunk_group(chunking_context, module_graph)
147            }
148        })
149    }
150}
151
152#[turbo_tasks::value_impl]
153impl Module for WorkerLoaderModule {
154    #[turbo_tasks::function]
155    async fn ident(&self) -> Result<Vc<AssetIdent>> {
156        Ok(self
157            .inner
158            .ident()
159            .owned()
160            .await?
161            .with_modifier(self.worker_type.modifier_str())
162            .into_vc())
163    }
164
165    #[turbo_tasks::function]
166    fn source(&self) -> Vc<turbopack_core::source::OptionSource> {
167        Vc::cell(None)
168    }
169
170    #[turbo_tasks::function]
171    async fn references(self: Vc<Self>) -> Result<Vc<ModuleReferences>> {
172        let this = self.await?;
173        Ok(Vc::cell(vec![ResolvedVc::upcast(
174            WorkerModuleReference::new(*ResolvedVc::upcast(this.inner), this.worker_type)
175                .to_resolved()
176                .await?,
177        )]))
178    }
179
180    #[turbo_tasks::function]
181    fn side_effects(self: Vc<Self>) -> Vc<ModuleSideEffects> {
182        ModuleSideEffects::SideEffectFree.cell()
183    }
184}
185
186#[turbo_tasks::value_impl]
187impl ChunkableModule for WorkerLoaderModule {
188    #[turbo_tasks::function]
189    fn as_chunk_item(
190        self: ResolvedVc<Self>,
191        module_graph: ResolvedVc<ModuleGraph>,
192        chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
193    ) -> Vc<Box<dyn turbopack_core::chunk::ChunkItem>> {
194        ecmascript_chunk_item(ResolvedVc::upcast(self), module_graph, chunking_context)
195    }
196}
197
198#[turbo_tasks::value_impl]
199impl EcmascriptChunkPlaceable for WorkerLoaderModule {
200    #[turbo_tasks::function]
201    fn get_exports(&self) -> Vc<EcmascriptExports> {
202        EcmascriptExports::Value.cell()
203    }
204
205    #[turbo_tasks::function]
206    async fn chunk_item_content(
207        self: Vc<Self>,
208        chunking_context: Vc<Box<dyn ChunkingContext>>,
209        module_graph: Vc<ModuleGraph>,
210        _async_module_info: Option<Vc<AsyncModuleInfo>>,
211        estimated: bool,
212    ) -> Result<Vc<EcmascriptChunkItemContent>> {
213        let this = self.await?;
214        let options = EcmascriptChunkItemOptions {
215            supports_arrow_functions: *chunking_context
216                .environment()
217                .runtime_versions()
218                .supports_arrow_functions()
219                .await?,
220            ..Default::default()
221        };
222
223        if estimated {
224            // In estimation mode we cannot call into chunking context APIs
225            // otherwise we will induce a turbo tasks cycle. But we only need an
226            // approximate solution. We'll use the same estimate for both web
227            // and Node.js workers.
228            return Ok(EcmascriptChunkItemContent {
229                inner_code: formatdoc! {
230                    r#"
231                        {TURBOPACK_EXPORT_VALUE}(function(Ctor, opts) {{
232                            return {TURBOPACK_CREATE_WORKER}(Ctor, __dirname + "/" + {worker_path:#}, opts);
233                        }});
234                    "#,
235                    worker_path = StringifyJs(&"a_fake_path_for_size_estimation"),
236                }
237                .into(),
238                options,
239                ..Default::default()
240            }
241            .cell());
242        }
243
244        let code = match this.worker_type {
245            WorkerType::WebWorker | WorkerType::SharedWebWorker => {
246                // For web workers, generate code that exports a function to create the worker.
247                // The function takes (WorkerConstructor, workerOptions) and calls createWorker
248                // with the entrypoint and chunks baked in.
249                let entrypoint_full_path = chunking_context.worker_entrypoint().path().await?;
250
251                // Get the entrypoint path relative to output root
252                let output_root = chunking_context.output_root().owned().await?;
253                let entrypoint_path = output_root
254                    .get_path_to(&entrypoint_full_path)
255                    .map(|s| s.to_string())
256                    .unwrap_or_else(|| entrypoint_full_path.path.to_string());
257
258                // Get the chunk data for the worker module
259                let chunks_data = self.chunks_data(chunking_context, module_graph).await?;
260                let chunks_data = chunks_data.iter().try_join().await?;
261                let chunks_data: Vec<_> = chunks_data
262                    .iter()
263                    .map(|chunk_data| EcmascriptChunkData::new(chunk_data))
264                    .collect();
265
266                formatdoc! {
267                    r#"
268                        {TURBOPACK_EXPORT_VALUE}(function(Ctor, opts) {{
269                            return {TURBOPACK_CREATE_WORKER}(Ctor, {entrypoint}, {chunks}, opts);
270                        }});
271                    "#,
272                    entrypoint = StringifyJs(&entrypoint_path),
273                    chunks = StringifyJs(&chunks_data),
274                }
275            }
276            WorkerType::NodeWorkerThread => {
277                // For Node.js workers, export a function to create the worker.
278                // The function takes (WorkerConstructor, workerOptions) and calls createWorker
279                // with the worker path baked in.
280                let chunk_group = self.chunk_group(chunking_context, module_graph).await?;
281                let assets = chunk_group.assets.await?;
282
283                // The last asset is the evaluate chunk (entry point) for the worker.
284                // The evaluated_chunk_group adds regular chunks first, then pushes the
285                // evaluate chunk last. The evaluate chunk contains the bootstrap code that
286                // loads the runtime and other chunks. For Node.js workers, we need a single
287                // file path (not a blob URL like browser workers), so we use the evaluate
288                // chunk which serves as the entry point.
289                let Some(entry_asset) = assets.last() else {
290                    bail!("cannot find worker entry point asset");
291                };
292                let entry_path = entry_asset.path().await?;
293
294                // Get the filename of the worker entry chunk
295                // We use just the filename because both the loader module and the worker
296                // entry chunk are in the same directory (typically server/chunks/), so we
297                // don't need a relative path - __dirname will already point to the correct
298                // directory
299                formatdoc! {
300                    r#"
301                        {TURBOPACK_EXPORT_VALUE}(function(Ctor, opts) {{
302                            return {TURBOPACK_CREATE_WORKER}(Ctor, __dirname + "/" + {worker_path:#}, opts);
303                        }});
304                    "#,
305                    worker_path = StringifyJs(entry_path.file_name()),
306                }
307            }
308        };
309
310        Ok(EcmascriptChunkItemContent {
311            inner_code: code.into(),
312            options,
313            ..Default::default()
314        }
315        .cell())
316    }
317
318    #[turbo_tasks::function]
319    fn chunk_item_output_assets(
320        self: Vc<Self>,
321        chunking_context: Vc<Box<dyn ChunkingContext>>,
322        module_graph: Vc<ModuleGraph>,
323    ) -> Vc<OutputAssetsWithReferenced> {
324        self.chunk_group_with_type(chunking_context, module_graph)
325    }
326}
327
328#[turbo_tasks::value]
329#[derive(ValueToString)]
330#[value_to_string("{} module", self.worker_type.friendly_str())]
331struct WorkerModuleReference {
332    module: ResolvedVc<Box<dyn Module>>,
333    worker_type: WorkerType,
334}
335
336#[turbo_tasks::value_impl]
337impl WorkerModuleReference {
338    #[turbo_tasks::function]
339    pub fn new(module: ResolvedVc<Box<dyn Module>>, worker_type: WorkerType) -> Vc<Self> {
340        Self::cell(WorkerModuleReference {
341            module,
342            worker_type,
343        })
344    }
345}
346
347#[turbo_tasks::value_impl]
348impl ModuleReference for WorkerModuleReference {
349    #[turbo_tasks::function]
350    fn resolve_reference(&self) -> Vc<ModuleResolveResult> {
351        *ModuleResolveResult::module(self.module)
352    }
353
354    fn chunking_type(&self) -> Option<ChunkingType> {
355        Some(ChunkingType::Isolated {
356            _ty: match self.worker_type {
357                WorkerType::SharedWebWorker | WorkerType::WebWorker => ChunkGroupType::Evaluated,
358                WorkerType::NodeWorkerThread => ChunkGroupType::Entry,
359            },
360            merge_tag: None,
361        })
362    }
363}