vercel/next.js · #99965

Turbopack: support turbopackIgnore on new URL()

lukesandberg · merged Nov 8, 202612 files · 195 + / 8 −
docs/01-app/03-api-reference/08-turbopack.mdx1 + / 1 −
@@ -127,7 +127,7 @@ Turbopack in Next.js has **zero-configuration** for the common use cases. Below  ### Magic Comments -Turbopack supports webpack-compatible magic comments for controlling import behavior. These comments work with dynamic `import()`, `require()`, `require.resolve()`, and `new Worker()` expressions (not static `import` statements).+Turbopack supports webpack-compatible magic comments for controlling import behavior. These comments work with dynamic `import()`, `require()`, `require.resolve()`, `new Worker()`, and `new URL()` expressions (not static `import` statements).  | Comment                   | Webpack | Turbopack | Description                    | | ------------------------- | ------- | --------- | ------------------------------ |
turbopack/crates/turbopack-core/src/issue/resolve.rs5 + / 0 −
@@ -24,6 +24,8 @@ pub struct ResolvingIssue {     pub resolve_options: ResolvedVc<ResolveOptions>,     pub error_message: Option<String>,     pub source: Option<IssueSource>,+    /// A suggestion for how to fix the issue, shown at the end of the description.+    pub hint: Option<RcStr>, }  #[async_trait]@@ -76,6 +78,9 @@ impl Issue for ResolvingIssue {                 }             }         }+        if let Some(hint) = &self.hint {+            writeln!(description, "{hint}")?;+        }         Ok(Some(StyledString::Text(description.into())))     } 
turbopack/crates/turbopack-core/src/resolve/error.rs45 + / 2 −
@@ -9,10 +9,10 @@ use crate::{         Issue, IssueExt, IssueSeverity, IssueSource, IssueStage, StyledString,         resolve::ResolvingIssue,     },-    reference_type::ReferenceType,+    reference_type::{EcmaScriptModulesReferenceSubType, ReferenceType, UrlReferenceSubType},     resolve::{         ModuleResolveResult, ModuleResolveResultItem, ResolveErrorMode, ResolveResult,-        options::ResolveOptions, parse::Request,+        options::ResolveOptions, parse::Request, pattern::Pattern,     }, }; @@ -186,6 +186,7 @@ async fn emit_resolve_error_issue(         resolve_options: resolve_options.to_resolved().await?,         error_message: Some(format!("{}", PrettyPrintError(&err))),         source,+        hint: resolve_hint(&reference_type, request).await?,     }     .resolved_cell()     .emit();@@ -216,12 +217,54 @@ async fn emit_unresolvable_issue(         resolve_options: resolve_options.to_resolved().await?,         error_message: None,         source,+        hint: resolve_hint(&reference_type, request).await?,     }     .resolved_cell()     .emit();     Ok(()) } +/// Suggests a fix for a request that failed because part of it is only known at runtime. Only+/// covers requests that accept a `turbopackIgnore` comment.+async fn resolve_hint(+    reference_type: &ReferenceType,+    request: Vc<Request>,+) -> Result<Option<RcStr>> {+    let example = match reference_type {+        ReferenceType::Url(UrlReferenceSubType::EcmaScriptNewUrl) => {+            "new URL(/* turbopackIgnore: true */ url, import.meta.url)"+        }+        ReferenceType::EcmaScriptModules(+            EcmaScriptModulesReferenceSubType::DynamicImport+            | EcmaScriptModulesReferenceSubType::LazyDynamicImport,+        ) => "import(/* turbopackIgnore: true */ path)",+        ReferenceType::CommonJs(_) => "require(/* turbopackIgnore: true */ path)",+        _ => return Ok(None),+    };+    let pattern = request.request_pattern().await?;+    if !pattern.has_dynamic_parts() {+        return Ok(None);+    }+    // The request failed as a whole, so none of its static alternatives resolved. They are+    // usually values the analyzer couldn't rule out, e.g. `'undefined'` for an unset option.+    let unresolved = match pattern.filter_static() {+        Some(Pattern::Alternatives(list)) => list+            .iter()+            .map(|p| p.describe_as_string())+            .collect::<Vec<_>>()+            .join(", "),+        Some(p) => p.describe_as_string(),+        None => return Ok(None),+    };+    Ok(Some(+        format!(+            "Turbopack couldn't resolve {unresolved}, which this request may evaluate to. If the \+             request is only known at runtime, opt out of bundling with `{example}`."+        )+        .into(),+    ))+}+ pub async fn resolve_error_severity(resolve_options: Vc<ResolveOptions>) -> Result<IssueSeverity> {     Ok(if resolve_options.await?.loose_errors {         IssueSeverity::Warning
turbopack/crates/turbopack-core/src/resolve/mod.rs5 + / 0 −
@@ -2133,6 +2133,7 @@ async fn resolve_internal_inline(                                     .to_string(),                             ),                             source: None,+                            hint: None,                         }                         .resolved_cell()                         .emit();@@ -2155,6 +2156,7 @@ async fn resolve_internal_inline(                         resolve_options: options.to_resolved().await?,                         error_message: Some("windows imports are not implemented yet".to_string()),                         source: None,+                        hint: None,                     }                     .resolved_cell()                     .emit();@@ -2250,6 +2252,7 @@ async fn resolve_internal_inline(                         resolve_options: options.to_resolved().await?,                         error_message: None,                         source: None,+                        hint: None,                     }                     .resolved_cell()                     .emit();@@ -2791,6 +2794,7 @@ async fn apply_in_package(             resolve_options: options.to_resolved().await?,             error_message: Some(format!("invalid alias field value: {value}")),             source: None,+            hint: None,         }         .resolved_cell()         .emit();@@ -3429,6 +3433,7 @@ async fn resolve_package_internal_with_imports_field(             resolve_options: resolve_options.to_resolved().await?,             error_message: None,             source: None,+            hint: None,         }         .resolved_cell()         .emit();
turbopack/crates/turbopack-ecmascript/src/analyzer/imports.rs4 + / 1 −
@@ -1468,10 +1468,13 @@ impl Visit for Analyzer<'_> {         n.visit_children_with(self);     } +    /// check if `new Worker()` or `new URL()` contains magic comments, e.g.+    /// - new Worker(/* turbopackIgnore: true */ new URL("./worker.js", import.meta.url))+    /// - new URL(/* turbopackIgnore: true */ url, import.meta.url)     fn visit_new_expr(&mut self, n: &NewExpr) {         if let Some(comments) = self.comments {             let callee_span = match &*n.callee {-                Expr::Ident(Ident { sym, .. }) if sym == "Worker" => Some(n.span),+                Expr::Ident(Ident { sym, .. }) if sym == "Worker" || sym == "URL" => Some(n.span),                 _ => None,             }; 
turbopack/crates/turbopack-ecmascript/src/references/mod.rs9 + / 2 −
@@ -2112,7 +2112,9 @@ where                             return Ok(());                         }                     }-                    let error_mode = if in_try {+                    let error_mode = if attributes.optional {+                        ResolveErrorMode::Ignore+                    } else if in_try {                         ResolveErrorMode::Warn                     } else {                         ResolveErrorMode::Error@@ -4383,7 +4385,12 @@ async fn value_visitor_inner<'a>(                 rcstr!("ignored SharedWorker constructor"),             ),             "define" => JsValue::WellKnownFunction(WellKnownFunctionKind::Define),-            "URL" => JsValue::WellKnownFunction(WellKnownFunctionKind::URLConstructor),+            "URL" => JsValue::unknown_if(+                ignore,+                JsValue::WellKnownFunction(WellKnownFunctionKind::URLConstructor),+                true,+                rcstr!("ignored URL constructor"),+            ),             "process" => JsValue::WellKnownObject(WellKnownObjectKind::NodeProcessModule),             "Object" => JsValue::WellKnownObject(WellKnownObjectKind::GlobalObject),             "Buffer" => JsValue::WellKnownObject(WellKnownObjectKind::NodeBuffer),
turbopack/crates/turbopack-test-utils/src/snapshot.rs5 + / 2 −
@@ -40,11 +40,14 @@ pub async fn snapshot_issues<I: IntoIterator<Item = ReadRef<PlainIssue>>>(     for plain_issue in captured_issues.into_iter() {         let title = styled_string_to_file_safe_string(&plain_issue.title)             .replace('/', "__")-            // We replace "*", "?", and '"' because they're not allowed in filenames on Windows.+            // We replace these characters because they're not allowed in filenames on Windows.             .replace('*', "__star__")             .replace('"', "__quo__")             .replace('?', "__q__")-            .replace(':', "__c__");+            .replace(':', "__c__")+            .replace('<', "__lt__")+            .replace('>', "__gt__")+            .replace('|', "__pipe__");         let title = if title.len() > 50 {             &title[0..50]         } else {
turbopack/crates/turbopack-tests/tests/execution/turbopack/basic/new-url-ignore/input/index.jsadded54 + / 0 −
@@ -0,0 +1,54 @@+function ignoredStaticUrl() {+  return new URL(/* turbopackIgnore: true */ './missing.txt', import.meta.url)+}++function ignoredDynamicUrl(name) {+  return new URL(/* turbopackIgnore: true */ name, import.meta.url)+}++function optionalPartiallyDynamicUrl(options = {}) {+  return new URL(/* turbopackOptional: true */ options.url, import.meta.url)+}++function partiallyDynamicUrl(options = {}) {+  // Not ignored, so the `undefined` alternative fails to resolve and is+  // reported with a hint suggesting `turbopackIgnore`.+  return new URL(options.url, import.meta.url)+}++// Never called: these only check that the hint names the matching magic comment.+export function partiallyDynamicImport(options = {}) {+  return import(options.path)+}++export function partiallyDynamicRequire(options = {}) {+  return require(options.path)+}++function expectUnbundledUrl(url, name) {+  expect(url).toBeInstanceOf(URL)+  expect(url.protocol).toBe('file:')+  expect(url.pathname).toMatch(new RegExp(`/new-url-ignore/input/${name}$`))+}++it('leaves an ignored static new URL() to runtime', () => {+  expectUnbundledUrl(ignoredStaticUrl(), 'missing.txt')+})++it('leaves an ignored dynamic new URL() to runtime', () => {+  expectUnbundledUrl(ignoredDynamicUrl('./runtime.txt'), 'runtime.txt')+})++it('supports turbopackOptional on new URL()', () => {+  expectUnbundledUrl(+    optionalPartiallyDynamicUrl({ url: './runtime.txt' }),+    'runtime.txt'+  )+})++it('keeps a partially dynamic new URL() working at runtime', () => {+  expectUnbundledUrl(+    partiallyDynamicUrl({ url: './runtime.txt' }),+    'runtime.txt'+  )+})
turbopack/crates/turbopack-tests/tests/execution/turbopack/basic/new-url-ignore/issues/__l___Module not found____c__ Can't resolve __c_(_-3abca2.txtadded22 + / 0 −
@@ -0,0 +1,22 @@+error - [resolve] /turbopack/crates/turbopack-tests/tests/execution/turbopack/basic/new-url-ignore/input/index.js:16:10  Module not found: Can't resolve (<dynamic> | 'undefined')+  +      12 | +      13 | function partiallyDynamicUrl(options = {}) {+      14 |   // Not ignored, so the `undefined` alternative fails to resolve and is+      15 |   // reported with a hint suggesting `turbopackIgnore`.+         +          v-----------------------------------v+      16 +   return new URL(options.url, import.meta.url)+         +          ^-----------------------------------^+      17 | }+      18 | +      19 | // Never called: these only check that the hint names the matching magic comment.+      20 | export function partiallyDynamicImport(options = {}) {+  +  Turbopack couldn't resolve 'undefined', which this request may evaluate to. If the request is only known at runtime, opt out of bundling with `new URL(/* turbopackIgnore: true */ url, import.meta.url)`.+  +  +  | It was not possible to find the requested file.+  | Parsed request as written in source code: dynamic or module "'undefined'"+  | Path where resolving has started: [project]/turbopack/crates/turbopack-tests/tests/execution/turbopack/basic/new-url-ignore/input/index.js+  | Type of request: url request+  |\ No newline at end of file
turbopack/crates/turbopack-tests/tests/execution/turbopack/basic/new-url-ignore/issues/__l___Module not found____c__ Can't resolve __c_(_-a6d01e.txtadded22 + / 0 −
@@ -0,0 +1,22 @@+error - [resolve] /turbopack/crates/turbopack-tests/tests/execution/turbopack/basic/new-url-ignore/input/index.js:21:10  Module not found: Can't resolve (<dynamic> | 'undefined')+  +      17 | }+      18 | +      19 | // Never called: these only check that the hint names the matching magic comment.+      20 | export function partiallyDynamicImport(options = {}) {+         +          v------------------v+      21 +   return import(options.path)+         +          ^------------------^+      22 | }+      23 | +      24 | export function partiallyDynamicRequire(options = {}) {+      25 |   return require(options.path)+  +  Turbopack couldn't resolve 'undefined', which this request may evaluate to. If the request is only known at runtime, opt out of bundling with `import(/* turbopackIgnore: true */ path)`.+  +  +  | It was not possible to find the requested file.+  | Parsed request as written in source code: dynamic or module "'undefined'"+  | Path where resolving has started: [project]/turbopack/crates/turbopack-tests/tests/execution/turbopack/basic/new-url-ignore/input/index.js+  | Type of request: EcmaScript Modules request+  |\ No newline at end of file
turbopack/crates/turbopack-tests/tests/execution/turbopack/basic/new-url-ignore/issues/__l___Module not found____c__ Can't resolve __c_(_-ca1488.txtadded22 + / 0 −
@@ -0,0 +1,22 @@+error - [resolve] /turbopack/crates/turbopack-tests/tests/execution/turbopack/basic/new-url-ignore/input/index.js:25:10  Module not found: Can't resolve (<dynamic> | 'undefined')+  +      21 |   return import(options.path)+      22 | }+      23 | +      24 | export function partiallyDynamicRequire(options = {}) {+         +          v-------------------v+      25 +   return require(options.path)+         +          ^-------------------^+      26 | }+      27 | +      28 | function expectUnbundledUrl(url, name) {+      29 |   expect(url).toBeInstanceOf(URL)+  +  Turbopack couldn't resolve 'undefined', which this request may evaluate to. If the request is only known at runtime, opt out of bundling with `require(/* turbopackIgnore: true */ path)`.+  +  +  | It was not possible to find the requested file.+  | Parsed request as written in source code: dynamic or module "'undefined'"+  | Path where resolving has started: [project]/turbopack/crates/turbopack-tests/tests/execution/turbopack/basic/new-url-ignore/input/index.js+  | Type of request: commonjs request+  |\ No newline at end of file
turbopack/crates/turbopack-tests/tests/execution/turbopack/resolving/dynamic/issues/__l___Module not found____c__ Can't resolve __c_('-3a7d6e.txtrenamed1 + / 0 −
@@ -12,6 +12,7 @@ warning - [resolve] /turbopack/crates/turbopack-tests/tests/execution/turbopack/       13 | }       14 |    +  Turbopack couldn't resolve 'cannot-resolve', which this request may evaluate to. If the request is only known at runtime, opt out of bundling with `require(/* turbopackIgnore: true */ path)`.         | It was not possible to find the requested file.