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};
12use turbo_tasks_fs::FileSystemPath;
13use turbopack_core::{
14    chunk::{ChunkableModule, ChunkableModuleReference, ChunkingContext, 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, handle_resolve_error,
22        origin::ResolveOrigin, parse::Request, pattern::Pattern, resolve_raw, url_resolve,
23    },
24};
25
26use crate::{
27    code_gen::{CodeGen, CodeGeneration, IntoCodeGenReference},
28    create_visitor,
29    references::{
30        AstPath,
31        pattern_mapping::{PatternMapping, ResolveType},
32    },
33    worker_chunk::{WorkerType, module::WorkerLoaderModule},
34};
35
36/// A unified reference to a Worker (web or Node.js) that creates an isolated chunk group
37/// for the worker module.
38#[turbo_tasks::value]
39#[derive(Hash, Debug)]
40pub struct WorkerAssetReference {
41    pub worker_type: WorkerType,
42    pub origin: ResolvedVc<Box<dyn ResolveOrigin>>,
43    pub request: WorkerRequest,
44    pub issue_source: IssueSource,
45    pub error_mode: ResolveErrorMode,
46    /// When true, skip creating WorkerLoaderModule and return the inner module directly.
47    /// This is used when we're only tracing dependencies, not generating code.
48    pub tracing_only: bool,
49}
50
51/// The request type varies between web and Node.js workers
52#[turbo_tasks::value]
53#[derive(Hash, Debug, Clone)]
54pub enum WorkerRequest {
55    /// Web workers use Request (URLs)
56    Url(ResolvedVc<Request>),
57    /// Node.js workers use Pattern (file paths) with a context directory that should be the server
58    /// working directory
59    Pattern {
60        context_dir: FileSystemPath,
61        path: ResolvedVc<Pattern>,
62        collect_affecting_sources: bool,
63    },
64}
65
66impl WorkerAssetReference {
67    pub fn new_web_worker(
68        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
69        request: ResolvedVc<Request>,
70        issue_source: IssueSource,
71        error_mode: ResolveErrorMode,
72        tracing_only: bool,
73        is_shared: bool,
74    ) -> Self {
75        WorkerAssetReference {
76            worker_type: if is_shared {
77                WorkerType::SharedWebWorker
78            } else {
79                WorkerType::WebWorker
80            },
81            origin,
82            request: WorkerRequest::Url(request),
83            issue_source,
84            error_mode,
85            tracing_only,
86        }
87    }
88
89    pub fn new_node_worker_thread(
90        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
91        context_dir: FileSystemPath,
92        path: ResolvedVc<Pattern>,
93        collect_affecting_sources: bool,
94        issue_source: IssueSource,
95        error_mode: ResolveErrorMode,
96        tracing_only: bool,
97    ) -> Self {
98        WorkerAssetReference {
99            worker_type: WorkerType::NodeWorkerThread,
100            origin,
101            request: WorkerRequest::Pattern {
102                context_dir,
103                path,
104                collect_affecting_sources,
105            },
106            issue_source,
107            error_mode,
108            tracing_only,
109        }
110    }
111}
112
113#[turbo_tasks::value_impl]
114impl ModuleReference for WorkerAssetReference {
115    #[turbo_tasks::function]
116    async fn resolve_reference(&self) -> Result<Vc<ModuleResolveResult>> {
117        let asset_context = self.origin.asset_context().to_resolved().await?;
118
119        let result = match (&self.worker_type, &self.request) {
120            (WorkerType::WebWorker | WorkerType::SharedWebWorker, WorkerRequest::Url(request)) => {
121                // Web worker resolution uses url_resolve
122                url_resolve(
123                    *self.origin,
124                    **request,
125                    self.worker_type.reference_type(),
126                    Some(self.issue_source),
127                    self.error_mode,
128                )
129            }
130            (
131                WorkerType::NodeWorkerThread,
132                WorkerRequest::Pattern {
133                    context_dir,
134                    path,
135                    collect_affecting_sources,
136                },
137            ) => {
138                // Node.js worker resolution uses resolve_raw
139                let result = resolve_raw(
140                    context_dir.clone(),
141                    **path,
142                    *collect_affecting_sources,
143                    /* force_in_lookup_dir */ false,
144                );
145                let reference_type = ReferenceType::Worker(WorkerReferenceSubType::NodeWorker);
146                let result = asset_context.process_resolve_result(result, reference_type.clone());
147
148                // Report an error if we cannot resolve
149                handle_resolve_error(
150                    result,
151                    reference_type.clone(),
152                    *self.origin,
153                    Request::parse(path.owned().await?),
154                    self.origin.resolve_options(),
155                    self.error_mode,
156                    Some(self.issue_source),
157                )
158                .await?
159            }
160            _ => {
161                // This should never happen due to our constructor functions
162                unreachable!("WorkerType and WorkerRequest mismatch");
163            }
164        };
165
166        // When tracing only (no code generation), return the resolved modules directly
167        // without wrapping them in WorkerLoaderModule
168        if self.tracing_only {
169            return Ok(result);
170        }
171
172        // Wrap each resolved module in a WorkerLoaderModule
173        let result_ref = result.await?;
174        let mut primary = Vec::with_capacity(result_ref.primary.len());
175
176        for (request_key, resolve_item) in result_ref.primary.iter() {
177            match resolve_item {
178                ModuleResolveResultItem::Module(module) => {
179                    let module_ident = module.ident().to_string().await?;
180
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                                format!(
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                                .into(),
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                                format!(
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                                .into(),
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
260impl WorkerAssetReference {
261    /// Downgrade errors to warnings if we are not in Error mode or if loose errors is enabled
262    async fn get_module_type_issue_severity(&self) -> Result<IssueSeverity> {
263        Ok(
264            if self.error_mode != ResolveErrorMode::Error
265                || self.origin.resolve_options().await?.loose_errors
266            {
267                IssueSeverity::Warning
268            } else {
269                IssueSeverity::Error
270            },
271        )
272    }
273}
274
275#[turbo_tasks::value_impl]
276impl ValueToString for WorkerAssetReference {
277    #[turbo_tasks::function]
278    async fn to_string(&self) -> Result<Vc<RcStr>> {
279        Ok(Vc::cell(
280            format!(
281                "new {}({})",
282                match self.worker_type {
283                    WorkerType::WebWorker => "WebWorker",
284                    WorkerType::SharedWebWorker => "SharedWorker",
285                    WorkerType::NodeWorkerThread => "NodeWorkerThread",
286                },
287                match &self.request {
288                    WorkerRequest::Url(request) => request.to_string().await?,
289                    WorkerRequest::Pattern { path, .. } => path.to_string().await?,
290                }
291            )
292            .into(),
293        ))
294    }
295}
296
297#[turbo_tasks::value_impl]
298impl ChunkableModuleReference for WorkerAssetReference {}
299
300impl IntoCodeGenReference for WorkerAssetReference {
301    fn into_code_gen_reference(
302        self,
303        path: AstPath,
304    ) -> (ResolvedVc<Box<dyn ModuleReference>>, CodeGen) {
305        let reference = self.resolved_cell();
306        (
307            ResolvedVc::upcast(reference),
308            CodeGen::WorkerAssetReferenceCodeGen(WorkerAssetReferenceCodeGen { reference, path }),
309        )
310    }
311}
312
313#[derive(
314    PartialEq, Eq, TraceRawVcs, ValueDebugFormat, NonLocalValue, Hash, Debug, Encode, Decode,
315)]
316pub struct WorkerAssetReferenceCodeGen {
317    reference: ResolvedVc<WorkerAssetReference>,
318    path: AstPath,
319}
320
321impl WorkerAssetReferenceCodeGen {
322    pub async fn code_generation(
323        &self,
324        chunking_context: Vc<Box<dyn ChunkingContext>>,
325    ) -> Result<CodeGeneration> {
326        let reference = self.reference.await?;
327
328        // Build the request for PatternMapping
329        let request = match &reference.request {
330            WorkerRequest::Url(request) => **request,
331            WorkerRequest::Pattern { path, .. } => Request::parse(path.owned().await?),
332        };
333
334        // Use PatternMapping to handle both single and multiple (dynamic) worker results
335        let pm = PatternMapping::resolve_request(
336            request,
337            *reference.origin,
338            chunking_context,
339            self.reference.resolve_reference(),
340            ResolveType::ChunkItem,
341        )
342        .await?;
343
344        // Transform `new Worker(url, opts)` into `require(id)(Worker, opts)`
345        // The loader module exports a function that creates the worker with all necessary
346        // configuration (entrypoint, chunks, forwarded globals, etc.)
347        let visitor = create_visitor!(self.path, visit_mut_expr, |expr: &mut Expr| {
348            let message = if let Expr::New(new_expr) = expr {
349                if let Some(args) = &mut new_expr.args {
350                    match args.first_mut() {
351                        Some(ExprOrSpread {
352                            spread: None,
353                            expr: url_expr,
354                        }) => {
355                            // Get the Worker constructor (callee)
356                            let constructor = new_expr.callee.take();
357
358                            // Build the require call for the loader module
359                            let require_call = pm.create_require(*url_expr.take());
360
361                            // Build the arguments: (WorkerConstructor, ...rest_args)
362                            let mut call_args = vec![ExprOrSpread {
363                                spread: None,
364                                expr: constructor,
365                            }];
366                            // Add any remaining arguments (e.g., worker options)
367                            call_args.extend(args.drain(1..));
368
369                            // Transform to: require(id)(Worker, opts)
370                            *expr = Expr::Call(CallExpr {
371                                span: new_expr.span,
372                                callee: Callee::Expr(Box::new(require_call)),
373                                args: call_args,
374                                ..Default::default()
375                            });
376                            return;
377                        }
378                        // These are SWC bugs: https://github.com/swc-project/swc/issues/5394
379                        Some(ExprOrSpread {
380                            spread: Some(_),
381                            expr: _,
382                        }) => "spread operator is illegal in new Worker() expressions.",
383                        _ => "new Worker() expressions require at least 1 argument",
384                    }
385                } else {
386                    "new Worker() expressions require at least 1 argument"
387                }
388            } else {
389                "visitor must be executed on a NewExpr"
390            };
391            *expr = *quote_expr!(
392                "(() => { throw new Error($message); })()",
393                message: Expr = Expr::Lit(Lit::Str(message.into()))
394            );
395        });
396
397        Ok(CodeGeneration::visitors(vec![visitor]))
398    }
399}