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