Repository: vercel/next.js
Pull request: [turbopack] Respect `{eval:true}` in worker_threads constructors

### What?

Fixed Turbopack incorrectly trying to resolve inline JavaScript code as module references when `new Worker()` is called with `{ eval: true }` option.

Now we skip creating a reference when `eval:true`, and report warnings if we cannot tell what the value is

### Why?

Libraries like jsPDF create Worker threads by passing inline JavaScript code as the first argument along with `{ eval: true }` as the second argument. Turbopack was incorrectly treating this inline code as a file path and attempting to resolve it as a module reference, causing build failures.

### How?

Added logic to detect when the `eval: true` option is passed to the Worker constructor. When this option is present, Turbopack now skips creating a worker reference since the first argument contains executable code rather than a file path. Added comprehensive test coverage using jsPDF to verify the fix works correctly.

Fixes #91642

Changed files:
filename: test/e2e/app-dir/node-worker-threads/app/api/jspdf-test/route.ts
patch: @@ -0,0 +1,20 @@
+import { NextResponse } from 'next/server'
+
+export async function GET() {
+  try {
+    const { jsPDF } = await import('jspdf')
+    const doc = new jsPDF()
+    doc.text('Hello world!', 10, 10)
+    const pdfData = doc.output('arraybuffer')
+
+    return NextResponse.json({
+      success: true,
+      size: pdfData.byteLength,
+    })
+  } catch (error) {
+    return NextResponse.json(
+      { success: false, error: String(error) },
+      { status: 500 }
+    )
+  }
+}

filename: test/e2e/app-dir/node-worker-threads/node-worker-threads.test.ts
patch: @@ -6,6 +6,7 @@ describe('node-worker-threads', () => {
     skipDeployment: true,
     dependencies: {
       pino: '9.6.0',
+      jspdf: '4.2.1',
     },
   })
 
@@ -58,6 +59,17 @@ describe('node-worker-threads', () => {
     expect(data.turbopackKeys).toEqual([])
   })
 
+  it('should handle jsPDF which uses Worker with eval: true (issue #91642)', async () => {
+    // jsPDF internally creates Worker threads with { eval: true }, passing
+    // inline JS code instead of a file path. Turbopack should not try to
+    // resolve the first argument as a module reference in this case.
+    const res = await next.fetch('/api/jspdf-test')
+    const data = await res.json()
+    expect(res.status).toBe(200)
+    expect(data.success).toBe(true)
+    expect(data.size).toBeGreaterThan(0)
+  })
+
   it('should handle PNG file import in worker', async () => {
     // Test that static assets (like PNG images) can be imported and used in workers
     // The server worker returns the PNG URL, then we fetch it from the client

filename: turbopack/crates/turbopack-core/src/resolve/parse.rs
patch: @@ -293,7 +293,12 @@ impl Request {
                 Request::Unknown { path } => {
                     path.push(item);
                 }
-                Request::DataUri { .. } | Request::Uri { .. } | Request::Dynamic => {
+                Request::DataUri { .. } | Request::Uri { .. } => {
+                    return Request::Dynamic;
+                }
+                Request::Dynamic => {
+                    // A dynamic prefix is essentially impossible to resolve so we don't try.  We
+                    // would have to scan the entire repo for suffix matches.
                     return Request::Dynamic;
                 }
                 Request::Alternatives { .. } => unreachable!(),

filename: turbopack/crates/turbopack-ecmascript/src/references/mod.rs
patch: @@ -2167,6 +2167,58 @@ where
             WellKnownFunctionKind::NodeWorkerConstructor => {
                 let args = linked_args().await?;
                 if !args.is_empty() {
+                    // When `{ eval: true }` is passed as the second argument,
+                    // the first argument is inline JS code, not a file path.
+                    // Skip creating a worker reference in that case.
+                    let mut dynamic_warning: Option<&str> = None;
+                    if let Some(opts) = args.get(1) {
+                        match opts {
+                            JsValue::Object { parts, .. } => {
+                                let eval_value = parts.iter().find_map(|part| match part {
+                                    ObjectPart::KeyValue(
+                                        JsValue::Constant(JsConstantValue::Str(key)),
+                                        value,
+                                    ) if key.as_str() == "eval" => Some(value),
+                                    _ => None,
+                                });
+                                if let Some(eval_value) = eval_value {
+                                    match eval_value {
+                                        // eval: true — first arg is code, not a
+                                        // path
+                                        JsValue::Constant(JsConstantValue::True) => {
+                                            return Ok(());
+                                        }
+                                        // eval: false — first arg is a path,
+                                        // continue normally
+                                        JsValue::Constant(JsConstantValue::False) => {}
+                                        // eval is set but not a literal boolean
+                                        _ => {
+                                            dynamic_warning = Some("has a dynamic `eval` option");
+                                        }
+                                    }
+                                }
+                            }
+                            // Options argument is not a static object literal —
+                            // we can't inspect it for `eval: true`
+                            _ => {
+                                dynamic_warning = Some("has a dynamic options argument");
+                            }
+                        }
+                    }
+                    if let Some(warning) = dynamic_warning {
+                        let (args, hints) = explain_args(args);
+                        handler.span_warn_with_code(
+                            span,
+                            &format!("new Worker({args}) {warning}{hints}"),
+                            DiagnosticId::Lint(
+                                errors::failed_to_analyze::ecmascript::NEW_WORKER.to_string(),
+                            ),
+                        );
+                        if ignore_dynamic_requests {
+                            return Ok(());
+                        }
+                    }
+
                     let pat = js_value_to_pattern(&args[0]);
                     if !pat.has_constant_parts() {
                         let (args, hints) = explain_args(args);
@@ -2208,7 +2260,7 @@ where
                     span,
                     &format!("new Worker({args}) is not statically analyze-able{hints}",),
                     DiagnosticId::Error(
-                        errors::failed_to_analyze::ecmascript::FS_METHOD.to_string(),
+                        errors::failed_to_analyze::ecmascript::NEW_WORKER.to_string(),
                     ),
                 );
                 // Ignore (e.g. dynamic parameter or string literal)