Skip to main content

turbopack_ecmascript/references/
worker.rs

1use anyhow::Result;
2use bincode::{Decode, Encode};
3use swc_core::{
4    common::util::take::Take,
5    ecma::ast::{CallExpr, Callee, Expr, ExprOrSpread, Lit},
6    quote_expr,
7};
8use turbo_rcstr::{RcStr, rcstr};
9use turbo_tasks::{
10    NonLocalValue, ResolvedVc, ValueToString, Vc, debug::ValueDebugFormat, trace::TraceRawVcs,
11    turbofmt,
12};
13use turbo_tasks_fs::FileSystemPath;
14use turbopack_core::{
15    chunk::{ChunkableModule, ChunkingContext, ChunkingType, EvaluatableAsset},
16    context::AssetContext,
17    issue::{IssueExt, IssueSeverity, IssueSource, StyledString, code_gen::CodeGenerationIssue},
18    module::Module,
19    reference::ModuleReference,
20    reference_type::{ReferenceType, WorkerReferenceSubType},
21    resolve::{
22        ModuleResolveResult, ModuleResolveResultItem, ResolveErrorMode,
23        error::handle_resolve_error, origin::ResolveOrigin, parse::Request, pattern::Pattern,
24        resolve_raw, url_resolve,
25    },
26};
27
28use crate::{
29    code_gen::{CodeGen, CodeGeneration, IntoCodeGenReference},
30    create_visitor,
31    references::{
32        AstPath,
33        pattern_mapping::{PatternMapping, ResolveType},
34    },
35    worker_chunk::{WorkerType, module::WorkerLoaderModule},
36};
37
38/// A unified reference to a Worker (web or Node.js) that creates an isolated chunk group
39/// for the worker module.
40#[turbo_tasks::value]
41#[derive(Hash, Debug)]
42pub struct WorkerAssetReference {
43    pub worker_type: WorkerType,
44    pub origin: ResolvedVc<Box<dyn ResolveOrigin>>,
45    pub request: WorkerRequest,
46    pub issue_source: IssueSource,
47    pub error_mode: ResolveErrorMode,
48    /// When true, skip creating WorkerLoaderModule and return the inner module directly.
49    /// This is used when we're only tracing dependencies, not generating code.
50    pub tracing_only: bool,
51}
52
53/// The request type varies between web and Node.js workers
54#[turbo_tasks::value]
55#[derive(Hash, Debug, Clone)]
56pub enum WorkerRequest {
57    /// Web workers use Request (URLs)
58    Url(ResolvedVc<Request>),
59    /// Node.js workers use Pattern (file paths) with a context directory that should be the server
60    /// working directory
61    Pattern {
62        context_dir: FileSystemPath,
63        path: ResolvedVc<Pattern>,
64        collect_affecting_sources: bool,
65    },
66}
67
68impl WorkerAssetReference {
69    pub fn new_web_worker(
70        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
71        request: ResolvedVc<Request>,
72        issue_source: IssueSource,
73        error_mode: ResolveErrorMode,
74        tracing_only: bool,
75        is_shared: bool,
76    ) -> Self {
77        WorkerAssetReference {
78            worker_type: if is_shared {
79                WorkerType::SharedWebWorker
80            } else {
81                WorkerType::WebWorker
82            },
83            origin,
84            request: WorkerRequest::Url(request),
85            issue_source,
86            error_mode,
87            tracing_only,
88        }
89    }
90
91    pub fn new_node_worker_thread(
92        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
93        context_dir: FileSystemPath,
94        path: ResolvedVc<Pattern>,
95        collect_affecting_sources: bool,
96        issue_source: IssueSource,
97        error_mode: ResolveErrorMode,
98        tracing_only: bool,
99    ) -> Self {
100        WorkerAssetReference {
101            worker_type: WorkerType::NodeWorkerThread,
102            origin,
103            request: WorkerRequest::Pattern {
104                context_dir,
105                path,
106                collect_affecting_sources,
107            },
108            issue_source,
109            error_mode,
110            tracing_only,
111        }
112    }
113}
114
115#[turbo_tasks::value_impl]
116impl ModuleReference for WorkerAssetReference {
117    #[turbo_tasks::function]
118    async fn resolve_reference(&self) -> Result<Vc<ModuleResolveResult>> {
119        let asset_context = self.origin.asset_context().to_resolved().await?;
120
121        let result = match (&self.worker_type, &self.request) {
122            (WorkerType::WebWorker | WorkerType::SharedWebWorker, WorkerRequest::Url(request)) => {
123                // Web worker resolution uses url_resolve
124                url_resolve(
125                    *self.origin,
126                    **request,
127                    self.worker_type.reference_type(),
128                    Some(self.issue_source),
129                    self.error_mode,
130                )
131            }
132            (
133                WorkerType::NodeWorkerThread,
134                WorkerRequest::Pattern {
135                    context_dir,
136                    path,
137                    collect_affecting_sources,
138                },
139            ) => {
140                // Node.js worker resolution uses resolve_raw
141                let result = resolve_raw(
142                    context_dir.clone(),
143                    **path,
144                    *collect_affecting_sources,
145                    /* force_in_lookup_dir */ false,
146                );
147                let reference_type = ReferenceType::Worker(WorkerReferenceSubType::NodeWorker);
148                let result = asset_context.process_resolve_result(result, reference_type.clone());
149
150                // Report an error if we cannot resolve
151                handle_resolve_error(
152                    result,
153                    reference_type.clone(),
154                    *self.origin,
155                    Request::parse(path.owned().await?),
156                    self.origin.resolve_options(),
157                    self.error_mode,
158                    Some(self.issue_source),
159                )
160                .await?
161            }
162            _ => {
163                // This should never happen due to our constructor functions
164                unreachable!("WorkerType and WorkerRequest mismatch");
165            }
166        };
167
168        // When tracing only (no code generation), return the resolved modules directly
169        // without wrapping them in WorkerLoaderModule
170        if self.tracing_only {
171            return Ok(result);
172        }
173
174        // Wrap each resolved module in a WorkerLoaderModule
175        let result_ref = result.await?;
176        let mut primary = Vec::with_capacity(result_ref.primary.len());
177
178        for (request_key, resolve_item) in result_ref.primary.iter() {
179            match resolve_item {
180                ModuleResolveResultItem::Module(module) => {
181                    let Some(chunkable) =
182                        ResolvedVc::try_downcast::<Box<dyn ChunkableModule>>(*module)
183                    else {
184                        CodeGenerationIssue {
185                            severity: self.get_module_type_issue_severity().await?,
186                            title: StyledString::Text(rcstr!("non-chunkable module"))
187                                .resolved_cell(),
188                            message: StyledString::Text(
189                                turbofmt!(
190                                    "Worker entry point module '{}' is not chunkable and cannot \
191                                     be used as a worker module. This may happen if the module \
192                                     type doesn't support bundling.",
193                                    module.ident()
194                                )
195                                .await?,
196                            )
197                            .resolved_cell(),
198                            path: self.origin.origin_path().owned().await?,
199                            source: Some(self.issue_source),
200                        }
201                        .resolved_cell()
202                        .emit();
203                        continue;
204                    };
205
206                    // For Node.js worker threads, the module must also be evaluatable since
207                    // it becomes an entry point
208                    if matches!(self.worker_type, WorkerType::NodeWorkerThread)
209                        && ResolvedVc::try_sidecast::<Box<dyn EvaluatableAsset>>(chunkable)
210                            .is_none()
211                    {
212                        CodeGenerationIssue {
213                            severity: self.get_module_type_issue_severity().await?,
214                            title: StyledString::Text(rcstr!("non-evaluatable module"))
215                                .resolved_cell(),
216                            message: StyledString::Text(
217                                turbofmt!(
218                                    "Worker thread entry point module '{}' must be evaluatable to \
219                                     serve as an entry point. This module cannot be used as a \
220                                     Node.js worker_threads Worker entry point because it doesn't \
221                                     support direct evaluation.",
222                                    module.ident()
223                                )
224                                .await?,
225                            )
226                            .resolved_cell(),
227                            path: self.origin.origin_path().owned().await?,
228                            source: Some(self.issue_source),
229                        }
230                        .resolved_cell()
231                        .emit();
232                        continue;
233                    }
234
235                    let loader =
236                        WorkerLoaderModule::new(*chunkable, self.worker_type, *asset_context)
237                            .to_resolved()
238                            .await?;
239
240                    primary.push((
241                        request_key.clone(),
242                        ModuleResolveResultItem::Module(ResolvedVc::upcast(loader)),
243                    ));
244                }
245                // Pass through other result types (External, Ignore, etc.)
246                _ => {
247                    primary.push((request_key.clone(), resolve_item.clone()));
248                }
249            }
250        }
251
252        Ok(ModuleResolveResult {
253            primary: primary.into_boxed_slice(),
254            affecting_sources: result_ref.affecting_sources.clone(),
255        }
256        .cell())
257    }
258
259    fn chunking_type(&self) -> Option<ChunkingType> {
260        Some(ChunkingType::Parallel {
261            inherit_async: false,
262            hoisted: false,
263        })
264    }
265}
266
267impl WorkerAssetReference {
268    /// Downgrade errors to warnings if we are not in Error mode or if loose errors is enabled
269    async fn get_module_type_issue_severity(&self) -> Result<IssueSeverity> {
270        Ok(
271            if self.error_mode != ResolveErrorMode::Error
272                || self.origin.resolve_options().await?.loose_errors
273            {
274                IssueSeverity::Warning
275            } else {
276                IssueSeverity::Error
277            },
278        )
279    }
280}
281
282#[turbo_tasks::value_impl]
283impl ValueToString for WorkerAssetReference {
284    #[turbo_tasks::function]
285    async fn to_string(&self) -> Result<Vc<RcStr>> {
286        let worker_type = match self.worker_type {
287            WorkerType::WebWorker => "WebWorker",
288            WorkerType::SharedWebWorker => "SharedWorker",
289            WorkerType::NodeWorkerThread => "NodeWorkerThread",
290        };
291        let request = match &self.request {
292            WorkerRequest::Url(request) => request.to_string(),
293            WorkerRequest::Pattern { path, .. } => path.to_string(),
294        };
295        Ok(Vc::cell(turbofmt!("new {worker_type}({request})").await?))
296    }
297}
298
299impl IntoCodeGenReference for WorkerAssetReference {
300    fn into_code_gen_reference(
301        self,
302        path: AstPath,
303    ) -> (ResolvedVc<Box<dyn ModuleReference>>, CodeGen) {
304        let reference = self.resolved_cell();
305        (
306            ResolvedVc::upcast(reference),
307            CodeGen::WorkerAssetReferenceCodeGen(WorkerAssetReferenceCodeGen { reference, path }),
308        )
309    }
310}
311
312#[derive(
313    PartialEq, Eq, TraceRawVcs, ValueDebugFormat, NonLocalValue, Hash, Debug, Encode, Decode,
314)]
315pub struct WorkerAssetReferenceCodeGen {
316    reference: ResolvedVc<WorkerAssetReference>,
317    path: AstPath,
318}
319
320impl WorkerAssetReferenceCodeGen {
321    pub async fn code_generation(
322        &self,
323        chunking_context: Vc<Box<dyn ChunkingContext>>,
324    ) -> Result<CodeGeneration> {
325        let reference = self.reference.await?;
326
327        // Build the request for PatternMapping
328        let request = match &reference.request {
329            WorkerRequest::Url(request) => **request,
330            WorkerRequest::Pattern { path, .. } => Request::parse(path.owned().await?),
331        };
332
333        // Use PatternMapping to handle both single and multiple (dynamic) worker results
334        let pm = PatternMapping::resolve_request(
335            request,
336            *reference.origin,
337            chunking_context,
338            self.reference.resolve_reference(),
339            ResolveType::ChunkItem,
340        )
341        .await?;
342
343        // Transform `new Worker(url, opts)` into `require(id)(Worker, opts)`
344        // The loader module exports a function that creates the worker with all necessary
345        // configuration (entrypoint, chunks, forwarded globals, etc.)
346        let visitor = create_visitor!(self.path, visit_mut_expr, |expr: &mut Expr| {
347            let message = if let Expr::New(new_expr) = expr {
348                if let Some(args) = &mut new_expr.args {
349                    match args.first_mut() {
350                        Some(ExprOrSpread {
351                            spread: None,
352                            expr: url_expr,
353                        }) => {
354                            // Get the Worker constructor (callee)
355                            let constructor = new_expr.callee.take();
356
357                            // Build the require call for the loader module
358                            let require_call = pm.create_require(*url_expr.take());
359
360                            // Build the arguments: (WorkerConstructor, ...rest_args)
361                            let mut call_args = vec![ExprOrSpread {
362                                spread: None,
363                                expr: constructor,
364                            }];
365                            // Add any remaining arguments (e.g., worker options)
366                            call_args.extend(args.drain(1..));
367
368                            // Transform to: require(id)(Worker, opts)
369                            *expr = Expr::Call(CallExpr {
370                                span: new_expr.span,
371                                callee: Callee::Expr(Box::new(require_call)),
372                                args: call_args,
373                                ..Default::default()
374                            });
375                            return;
376                        }
377                        // These are SWC bugs: https://github.com/swc-project/swc/issues/5394
378                        Some(ExprOrSpread {
379                            spread: Some(_),
380                            expr: _,
381                        }) => "spread operator is illegal in new Worker() expressions.",
382                        _ => "new Worker() expressions require at least 1 argument",
383                    }
384                } else {
385                    "new Worker() expressions require at least 1 argument"
386                }
387            } else {
388                "visitor must be executed on a NewExpr"
389            };
390            *expr = *quote_expr!(
391                "(() => { throw new Error($message); })()",
392                message: Expr = Expr::Lit(Lit::Str(message.into()))
393            );
394        });
395
396        Ok(CodeGeneration::visitors(vec![visitor]))
397    }
398}