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