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