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