Skip to main content

turbopack_ecmascript/references/
worker.rs

1use anyhow::Result;
2use bincode::{Decode, Encode};
3use swc_core::{
4    common::{DUMMY_SP, util::take::Take},
5    ecma::ast::{ArrayLit, CallExpr, Callee, Expr, ExprOrSpread, Lit, Null},
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 origin = self.origin.into_trait_ref().await?;
120        let asset_context = origin.asset_context();
121
122        let result = match (&self.worker_type, &self.request) {
123            (WorkerType::WebWorker | WorkerType::SharedWebWorker, WorkerRequest::Url(request)) => {
124                // Web worker resolution uses url_resolve
125                url_resolve(
126                    *self.origin,
127                    **request,
128                    self.worker_type.reference_type(),
129                    Some(self.issue_source),
130                    self.error_mode,
131                )
132            }
133            (
134                WorkerType::NodeWorkerThread,
135                WorkerRequest::Pattern {
136                    context_dir,
137                    path,
138                    collect_affecting_sources,
139                },
140            ) => {
141                // Node.js worker resolution uses resolve_raw
142                let result = resolve_raw(
143                    context_dir.clone(),
144                    **path,
145                    *collect_affecting_sources,
146                    /* force_in_lookup_dir */ false,
147                );
148                let reference_type = ReferenceType::Worker(WorkerReferenceSubType::NodeWorker);
149                let result = asset_context.process_resolve_result(result, reference_type.clone());
150
151                // Report an error if we cannot resolve
152                handle_resolve_error(
153                    result,
154                    reference_type.clone(),
155                    origin.origin_path(),
156                    Request::parse(path.owned().await?),
157                    origin.resolve_options(),
158                    self.error_mode,
159                    Some(self.issue_source),
160                )
161                .await?
162            }
163            _ => {
164                // This should never happen due to our constructor functions
165                unreachable!("WorkerType and WorkerRequest mismatch");
166            }
167        };
168
169        // When tracing only (no code generation), return the resolved modules directly
170        // without wrapping them in WorkerLoaderModule
171        if self.tracing_only {
172            return Ok(result);
173        }
174
175        // Wrap each resolved module in a WorkerLoaderModule
176        let result_ref = result.await?;
177        let mut primary = Vec::with_capacity(result_ref.primary.len());
178
179        for (request_key, resolve_item) in result_ref.primary.iter() {
180            match resolve_item {
181                ModuleResolveResultItem::Module(module) => {
182                    let Some(chunkable) =
183                        ResolvedVc::try_downcast::<Box<dyn ChunkableModule>>(*module)
184                    else {
185                        CodeGenerationIssue {
186                            severity: self.get_module_type_issue_severity().await?,
187                            title: StyledString::Text(rcstr!("non-chunkable module"))
188                                .resolved_cell(),
189                            message: StyledString::Text(
190                                turbofmt!(
191                                    "Worker entry point module '{}' is not chunkable and cannot \
192                                     be used as a worker module. This may happen if the module \
193                                     type doesn't support bundling.",
194                                    module.ident()
195                                )
196                                .await?,
197                            )
198                            .resolved_cell(),
199                            path: origin.origin_path(),
200                            source: Some(self.issue_source),
201                        }
202                        .resolved_cell()
203                        .emit();
204                        continue;
205                    };
206
207                    // For Node.js worker threads, the module must also be evaluatable since
208                    // it becomes an entry point
209                    if matches!(self.worker_type, WorkerType::NodeWorkerThread)
210                        && ResolvedVc::try_sidecast::<Box<dyn EvaluatableAsset>>(chunkable)
211                            .is_none()
212                    {
213                        CodeGenerationIssue {
214                            severity: self.get_module_type_issue_severity().await?,
215                            title: StyledString::Text(rcstr!("non-evaluatable module"))
216                                .resolved_cell(),
217                            message: StyledString::Text(
218                                turbofmt!(
219                                    "Worker thread entry point module '{}' must be evaluatable to \
220                                     serve as an entry point. This module cannot be used as a \
221                                     Node.js worker_threads Worker entry point because it doesn't \
222                                     support direct evaluation.",
223                                    module.ident()
224                                )
225                                .await?,
226                            )
227                            .resolved_cell(),
228                            path: origin.origin_path(),
229                            source: Some(self.issue_source),
230                        }
231                        .resolved_cell()
232                        .emit();
233                        continue;
234                    }
235
236                    let loader =
237                        WorkerLoaderModule::new(*chunkable, self.worker_type, *asset_context)
238                            .to_resolved()
239                            .await?;
240
241                    primary.push((
242                        request_key.clone(),
243                        ModuleResolveResultItem::Module(ResolvedVc::upcast(loader)),
244                    ));
245                }
246                // Pass through other result types (External, Ignore, etc.)
247                _ => {
248                    primary.push((request_key.clone(), resolve_item.clone()));
249                }
250            }
251        }
252
253        Ok(ModuleResolveResult {
254            primary: primary.into_boxed_slice(),
255            affecting_sources: result_ref.affecting_sources.clone(),
256        }
257        .cell())
258    }
259
260    fn chunking_type(&self) -> Option<ChunkingType> {
261        Some(ChunkingType::Parallel {
262            inherit_async: false,
263            hoisted: false,
264        })
265    }
266
267    fn source(&self) -> Option<IssueSource> {
268        Some(self.issue_source)
269    }
270}
271
272impl WorkerAssetReference {
273    /// Downgrade errors to warnings if we are not in Error mode or if loose errors is enabled
274    async fn get_module_type_issue_severity(&self) -> Result<IssueSeverity> {
275        Ok(
276            if self.error_mode != ResolveErrorMode::Error
277                || self
278                    .origin
279                    .into_trait_ref()
280                    .await?
281                    .resolve_options()
282                    .await?
283                    .loose_errors
284            {
285                IssueSeverity::Warning
286            } else {
287                IssueSeverity::Error
288            },
289        )
290    }
291}
292
293#[turbo_tasks::value_impl]
294impl ValueToString for WorkerAssetReference {
295    #[turbo_tasks::function]
296    async fn to_string(&self) -> Result<Vc<RcStr>> {
297        let worker_type = match self.worker_type {
298            WorkerType::WebWorker => "WebWorker",
299            WorkerType::SharedWebWorker => "SharedWorker",
300            WorkerType::NodeWorkerThread => "NodeWorkerThread",
301        };
302        let request = match &self.request {
303            WorkerRequest::Url(request) => request.to_string(),
304            WorkerRequest::Pattern { path, .. } => path.to_string(),
305        };
306        Ok(Vc::cell(turbofmt!("new {worker_type}({request})").await?))
307    }
308}
309
310impl IntoCodeGenReference for WorkerAssetReference {
311    fn into_code_gen_reference(
312        self,
313        path: AstPath,
314    ) -> (ResolvedVc<Box<dyn ModuleReference>>, CodeGen) {
315        let reference = self.resolved_cell();
316        (
317            ResolvedVc::upcast(reference),
318            CodeGen::WorkerAssetReferenceCodeGen(WorkerAssetReferenceCodeGen { reference, path }),
319        )
320    }
321}
322
323#[derive(
324    PartialEq, Eq, TraceRawVcs, ValueDebugFormat, NonLocalValue, Hash, Debug, Encode, Decode,
325)]
326pub struct WorkerAssetReferenceCodeGen {
327    reference: ResolvedVc<WorkerAssetReference>,
328    path: AstPath,
329}
330
331impl WorkerAssetReferenceCodeGen {
332    pub async fn code_generation(
333        &self,
334        chunking_context: Vc<Box<dyn ChunkingContext>>,
335    ) -> Result<CodeGeneration> {
336        let reference = self.reference.await?;
337
338        // Build the request for PatternMapping
339        let request = match &reference.request {
340            WorkerRequest::Url(request) => **request,
341            WorkerRequest::Pattern { path, .. } => Request::parse(path.owned().await?),
342        };
343
344        // Use PatternMapping to handle both single and multiple (dynamic) worker results
345        let pm = PatternMapping::resolve_request(
346            request,
347            *reference.origin,
348            chunking_context,
349            self.reference.resolve_reference(),
350            ResolveType::ChunkItem,
351        )
352        .await?;
353
354        // Transform `new Worker(url, opts)` into `require(id)(Worker, opts)`
355        // The loader module exports a function that creates the worker with all necessary
356        // configuration (entrypoint, chunks, forwarded globals, etc.)
357        let visitor = create_visitor!(self.path, visit_mut_expr, |expr: &mut Expr| {
358            let message = if let Expr::New(new_expr) = expr {
359                if let Some(args) = &mut new_expr.args {
360                    match args.first_mut() {
361                        Some(ExprOrSpread {
362                            spread: None,
363                            expr: url_expr,
364                        }) => {
365                            // Get the Worker constructor (callee)
366                            let constructor = new_expr.callee.take();
367
368                            // Build the require call for the loader module
369                            let require_call = pm.create_require(*url_expr.take());
370
371                            // Build the arguments: (WorkerConstructor, ...rest_args)
372                            let mut call_args = vec![ExprOrSpread {
373                                spread: None,
374                                expr: constructor,
375                            }];
376                            // Add any remaining arguments (e.g., worker options)
377                            call_args.extend(args.drain(1..));
378
379                            // Transform to: require(id)(Worker, opts)
380                            *expr = Expr::Call(CallExpr {
381                                span: new_expr.span,
382                                callee: Callee::Expr(Box::new(require_call)),
383                                args: call_args,
384                                ..Default::default()
385                            });
386                            return;
387                        }
388                        // These are SWC bugs: https://github.com/swc-project/swc/issues/5394
389                        Some(ExprOrSpread {
390                            spread: Some(_),
391                            expr: _,
392                        }) => "spread operator is illegal in new Worker() expressions.",
393                        _ => "new Worker() expressions require at least 1 argument",
394                    }
395                } else {
396                    "new Worker() expressions require at least 1 argument"
397                }
398            } else {
399                "visitor must be executed on a NewExpr"
400            };
401            *expr = *quote_expr!(
402                "(() => { throw new Error($message); })()",
403                message: Expr = Expr::Lit(Lit::Str(message.into()))
404            );
405        });
406
407        Ok(CodeGeneration::visitors(vec![visitor]))
408    }
409}
410
411#[derive(
412    PartialEq, Eq, TraceRawVcs, ValueDebugFormat, NonLocalValue, Debug, Hash, Encode, Decode,
413)]
414pub enum WorkerGlobalPlaceholder {
415    /// `const _TURBOPACK_WORKER_FORWARDED_GLOBALS_ = []`
416    ForwardedGlobals,
417    /// `const _TURBOPACK_WORKER_BASE_PATH_ = '_TURBOPACK_WORKER_BASE_PATH_REPLACE_'`
418    BasePath,
419}
420
421#[derive(
422    PartialEq, Eq, TraceRawVcs, ValueDebugFormat, NonLocalValue, Debug, Hash, Encode, Decode,
423)]
424pub struct WorkerGlobalsReplacementCodeGen {
425    /// Which placeholder this codegen replaces (determines the injected value).
426    placeholder: WorkerGlobalPlaceholder,
427    path: AstPath,
428}
429
430impl WorkerGlobalsReplacementCodeGen {
431    pub fn new(placeholder: WorkerGlobalPlaceholder, path: AstPath) -> Self {
432        WorkerGlobalsReplacementCodeGen { placeholder, path }
433    }
434
435    pub async fn code_generation(
436        &self,
437        chunking_context: Vc<Box<dyn ChunkingContext>>,
438    ) -> Result<CodeGeneration> {
439        let options = chunking_context.worker_configuration_options().await?;
440        let value: Expr = match self.placeholder {
441            WorkerGlobalPlaceholder::ForwardedGlobals => Expr::Array(ArrayLit {
442                span: DUMMY_SP,
443                elems: options
444                    .forwarded_globals
445                    .iter()
446                    .map(|global| Some(Expr::Lit(Lit::Str(global.as_str().into())).into()))
447                    .collect(),
448            }),
449            WorkerGlobalPlaceholder::BasePath => match &options.asset_prefix {
450                Some(asset_prefix) => Expr::Lit(Lit::Str(asset_prefix.as_str().into())),
451                None => Expr::Lit(Lit::Null(Null { span: DUMMY_SP })),
452            },
453        };
454
455        let visitor = create_visitor!(self.path, visit_mut_expr, |expr: &mut Expr| {
456            *expr = value.clone();
457        });
458
459        Ok(CodeGeneration::visitors(vec![visitor]))
460    }
461}
462
463impl From<WorkerGlobalsReplacementCodeGen> for CodeGen {
464    fn from(val: WorkerGlobalsReplacementCodeGen) -> Self {
465        CodeGen::WorkerGlobalsReplacementCodeGen(val)
466    }
467}