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