Skip to main content

turbopack_ecmascript/worker_chunk/
chunk_item.rs

1use anyhow::{Result, bail};
2use indoc::formatdoc;
3use turbo_rcstr::rcstr;
4use turbo_tasks::{ResolvedVc, TryJoinIterExt, Vc};
5use turbopack_core::{
6    chunk::{
7        AsyncModuleInfo, ChunkData, ChunkItem, ChunkType, ChunkingContext, ChunkingContextExt,
8        ChunksData, EvaluatableAsset, EvaluatableAssets, availability_info::AvailabilityInfo,
9    },
10    context::AssetContext,
11    ident::AssetIdent,
12    module::Module,
13    module_graph::{ModuleGraph, chunk_group_info::ChunkGroup},
14    output::{OutputAsset, OutputAssets, OutputAssetsReference, OutputAssetsWithReferenced},
15};
16
17use super::{module::WorkerLoaderModule, worker_type::WorkerType};
18use crate::{
19    chunk::{
20        EcmascriptChunkItem, EcmascriptChunkItemContent, EcmascriptChunkType,
21        data::EcmascriptChunkData,
22    },
23    runtime_functions::{TURBOPACK_CREATE_WORKER, TURBOPACK_EXPORT_VALUE},
24    utils::StringifyJs,
25};
26
27#[turbo_tasks::value(shared)]
28pub struct WorkerLoaderChunkItem {
29    pub module: ResolvedVc<WorkerLoaderModule>,
30    pub module_graph: ResolvedVc<ModuleGraph>,
31    pub chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
32    pub worker_type: WorkerType,
33    pub asset_context: ResolvedVc<Box<dyn AssetContext>>,
34}
35
36#[turbo_tasks::value_impl]
37impl WorkerLoaderChunkItem {
38    #[turbo_tasks::function]
39    async fn chunk_group(&self) -> Result<Vc<OutputAssetsWithReferenced>> {
40        let module = self.module.await?;
41
42        Ok(match self.worker_type {
43            WorkerType::WebWorker | WorkerType::SharedWebWorker => {
44                self.chunking_context.evaluated_chunk_group_assets(
45                    module
46                        .inner
47                        .ident()
48                        .with_modifier(self.worker_type.chunk_modifier_str()),
49                    ChunkGroup::Isolated(ResolvedVc::upcast(module.inner)),
50                    *self.module_graph,
51                    AvailabilityInfo::root(),
52                )
53            }
54            // WorkerThreads are treated as an entry point, webworkers probably should too but
55            // currently it would lead to a cascade that we need to address.
56            WorkerType::NodeWorkerThread => {
57                let Some(evaluatable) =
58                    ResolvedVc::try_sidecast::<Box<dyn EvaluatableAsset>>(module.inner)
59                else {
60                    bail!("Worker module must be evaluatable");
61                };
62
63                let worker_path = self
64                    .chunking_context
65                    .chunk_path(
66                        None,
67                        module.inner.ident(),
68                        Some(rcstr!("[worker thread]")),
69                        rcstr!(".js"),
70                    )
71                    .owned()
72                    .await?;
73
74                let entry_result = self
75                    .chunking_context
76                    .root_entry_chunk_group(
77                        worker_path,
78                        EvaluatableAssets::one(*evaluatable),
79                        *self.module_graph,
80                        OutputAssets::empty(),
81                        OutputAssets::empty(),
82                    )
83                    .await?;
84
85                OutputAssetsWithReferenced {
86                    assets: ResolvedVc::cell(vec![entry_result.asset]),
87                    referenced_assets: ResolvedVc::cell(vec![]),
88                    references: ResolvedVc::cell(vec![]),
89                }
90                .cell()
91            }
92        })
93    }
94
95    #[turbo_tasks::function]
96    async fn chunks_data(self: Vc<Self>) -> Result<Vc<ChunksData>> {
97        let this = self.await?;
98        Ok(ChunkData::from_assets(
99            this.chunking_context.output_root().owned().await?,
100            *self.chunk_group().await?.assets,
101        ))
102    }
103}
104
105#[turbo_tasks::value_impl]
106impl EcmascriptChunkItem for WorkerLoaderChunkItem {
107    #[turbo_tasks::function]
108    fn content(self: Vc<Self>) -> Vc<EcmascriptChunkItemContent> {
109        panic!("should not be called");
110    }
111
112    #[turbo_tasks::function]
113    async fn content_with_async_module_info(
114        self: Vc<Self>,
115        _async_module_info: Option<Vc<AsyncModuleInfo>>,
116        estimated: bool,
117    ) -> Result<Vc<EcmascriptChunkItemContent>> {
118        let this = self.await?;
119
120        if estimated {
121            // In estimation mode we cannot call into chunking context APIs
122            // otherwise we will induce a turbo tasks cycle. But we only need an
123            // approximate solution. We'll use the same estimate for both web
124            // and Node.js workers.
125            return Ok(EcmascriptChunkItemContent {
126                inner_code: formatdoc! {
127                    r#"
128                        {TURBOPACK_EXPORT_VALUE}(function(Ctor, opts) {{
129                            return {TURBOPACK_CREATE_WORKER}(Ctor, __dirname + "/" + {worker_path:#}, opts);
130                        }});
131                    "#,
132                    worker_path = StringifyJs(&"a_fake_path_for_size_estimation"),
133                }.into(),
134                ..Default::default()
135            }
136            .cell());
137        }
138
139        let code = match this.worker_type {
140            WorkerType::WebWorker | WorkerType::SharedWebWorker => {
141                // For web workers, generate code that exports a function to create the worker.
142                // The function takes (WorkerConstructor, workerOptions) and calls createWorker
143                // with the entrypoint and chunks baked in.
144                let entrypoint_full_path = this.chunking_context.worker_entrypoint().path().await?;
145
146                // Get the entrypoint path relative to output root
147                let output_root = this.chunking_context.output_root().owned().await?;
148                let entrypoint_path = output_root
149                    .get_path_to(&entrypoint_full_path)
150                    .map(|s| s.to_string())
151                    .unwrap_or_else(|| entrypoint_full_path.path.to_string());
152
153                // Get the chunk data for the worker module
154                let chunks_data = self.chunks_data().await?;
155                let chunks_data = chunks_data.iter().try_join().await?;
156                let chunks_data: Vec<_> = chunks_data
157                    .iter()
158                    .map(|chunk_data| EcmascriptChunkData::new(chunk_data))
159                    .collect();
160
161                formatdoc! {
162                    r#"
163                        {TURBOPACK_EXPORT_VALUE}(function(Ctor, opts) {{
164                            return {TURBOPACK_CREATE_WORKER}(Ctor, {entrypoint}, {chunks}, opts);
165                        }});
166                    "#,
167                    entrypoint = StringifyJs(&entrypoint_path),
168                    chunks = StringifyJs(&chunks_data),
169                }
170            }
171            WorkerType::NodeWorkerThread => {
172                // For Node.js workers, export a function to create the worker.
173                // The function takes (WorkerConstructor, workerOptions) and calls createWorker
174                // with the worker path baked in.
175                let chunk_group = self.chunk_group().await?;
176                let assets = chunk_group.assets.await?;
177
178                // The last asset is the evaluate chunk (entry point) for the worker.
179                // The evaluated_chunk_group adds regular chunks first, then pushes the
180                // evaluate chunk last. The evaluate chunk contains the bootstrap code that
181                // loads the runtime and other chunks. For Node.js workers, we need a single
182                // file path (not a blob URL like browser workers), so we use the evaluate
183                // chunk which serves as the entry point.
184                let Some(entry_asset) = assets.last() else {
185                    bail!("cannot find worker entry point asset");
186                };
187                let entry_path = entry_asset.path().await?;
188
189                // Get the filename of the worker entry chunk
190                // We use just the filename because both the loader module and the worker
191                // entry chunk are in the same directory (typically server/chunks/), so we
192                // don't need a relative path - __dirname will already point to the correct
193                // directory
194                formatdoc! {
195                    r#"
196                        {TURBOPACK_EXPORT_VALUE}(function(Ctor, opts) {{
197                            return {TURBOPACK_CREATE_WORKER}(Ctor, __dirname + "/" + {worker_path:#}, opts);
198                        }});
199                    "#,
200                    worker_path = StringifyJs(entry_path.file_name()),
201                }
202            }
203        };
204
205        Ok(EcmascriptChunkItemContent {
206            inner_code: code.into(),
207            ..Default::default()
208        }
209        .cell())
210    }
211}
212
213#[turbo_tasks::value_impl]
214impl OutputAssetsReference for WorkerLoaderChunkItem {
215    #[turbo_tasks::function]
216    async fn references(self: Vc<Self>) -> Result<Vc<OutputAssetsWithReferenced>> {
217        let this = self.await?;
218        match this.worker_type {
219            WorkerType::WebWorker | WorkerType::SharedWebWorker => Ok(self
220                .chunk_group()
221                .concatenate_asset(this.chunking_context.worker_entrypoint())),
222            WorkerType::NodeWorkerThread => {
223                // Node.js workers don't need a separate entrypoint asset
224                Ok(self.chunk_group())
225            }
226        }
227    }
228}
229
230#[turbo_tasks::value_impl]
231impl ChunkItem for WorkerLoaderChunkItem {
232    #[turbo_tasks::function]
233    fn asset_ident(&self) -> Vc<AssetIdent> {
234        self.module.ident()
235    }
236
237    #[turbo_tasks::function]
238    fn content_ident(&self) -> Vc<AssetIdent> {
239        self.module.ident()
240    }
241
242    #[turbo_tasks::function]
243    fn chunking_context(&self) -> Vc<Box<dyn ChunkingContext>> {
244        *self.chunking_context
245    }
246
247    #[turbo_tasks::function]
248    async fn ty(&self) -> Result<Vc<Box<dyn ChunkType>>> {
249        Ok(Vc::upcast(
250            Vc::<EcmascriptChunkType>::default().resolve().await?,
251        ))
252    }
253
254    #[turbo_tasks::function]
255    fn module(&self) -> Vc<Box<dyn Module>> {
256        *ResolvedVc::upcast(self.module)
257    }
258}