vercel/next.js · #99965
Turbopack: support turbopackIgnore on new URL()
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::Warningturbopack/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 fileturbopack/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 fileturbopack/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 fileturbopack/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.