Repository: vercel/next.js
Pull request: Turbopack: fix `@opentelemetry/api` resolve fallback

Previously, it tried `[project]/node_modules/@opentelemetry/api` and then fell back to `node_modules/next/dist/compiled/@opentelemetry/api`.

But that doesn't work if the dependency is installed in a monorepo subpackage and should have been resolved from `packages/my-app/node_modules/@opentelemetry/api`.

We already have `fallback_import_map` which is exactly what's needed here.

Closes PACK-5054

Changed files:
filename: crates/next-core/src/next_edge/context.rs
patch: @@ -22,7 +22,7 @@ use crate::{
     mode::NextMode,
     next_config::NextConfig,
     next_font::local::NextFontLocalResolvePlugin,
-    next_import_map::get_next_edge_import_map,
+    next_import_map::{get_next_edge_and_server_fallback_import_map, get_next_edge_import_map},
     next_server::context::ServerContextType,
     next_shared::resolve::{
         ModuleFeatureReportResolvePlugin, NextSharedRuntimeResolvePlugin,
@@ -94,6 +94,10 @@ pub async fn get_edge_resolve_options_context(
     )
     .to_resolved()
     .await?;
+    let next_edge_fallback_import_map =
+        get_next_edge_and_server_fallback_import_map(project_path.clone(), NextRuntime::Edge)
+            .to_resolved()
+            .await?;
 
     let mut before_resolve_plugins = vec![ResolvedVc::upcast(
         ModuleFeatureReportResolvePlugin::new(project_path.clone())
@@ -158,6 +162,7 @@ pub async fn get_edge_resolve_options_context(
         enable_edge_node_externals: true,
         custom_conditions,
         import_map: Some(next_edge_import_map),
+        fallback_import_map: Some(next_edge_fallback_import_map),
         module: true,
         browser: true,
         after_resolve_plugins,

filename: crates/next-core/src/next_import_map.rs
patch: @@ -516,6 +516,32 @@ pub async fn get_next_edge_import_map(
     Ok(import_map.cell())
 }
 
+/// Computes the Next-specific server-side and edge-side fallback import map.
+#[turbo_tasks::function]
+pub async fn get_next_edge_and_server_fallback_import_map(
+    project_path: FileSystemPath,
+    runtime: NextRuntime,
+) -> Result<Vc<ImportMap>> {
+    let mut fallback_import_map = ImportMap::empty();
+
+    let external_cjs_if_node = move |context_dir: FileSystemPath, request: &str| match runtime {
+        NextRuntime::Edge => request_to_import_mapping(context_dir, request),
+        NextRuntime::NodeJs => external_request_to_cjs_import_mapping(context_dir, request),
+    };
+
+    fallback_import_map.insert_exact_alias(
+        "@opentelemetry/api",
+        // It needs to prefer the local version of @opentelemetry/api, so put this in the fallback
+        // import map
+        ImportMapping::Alternatives(vec![external_cjs_if_node(
+            project_path,
+            "next/dist/compiled/@opentelemetry/api",
+        )])
+        .resolved_cell(),
+    );
+    Ok(fallback_import_map.cell())
+}
+
 /// Insert default aliases for the node.js's internal to raise unsupported
 /// runtime errors. User may provide polyfills for their own by setting user
 /// config's alias.
@@ -604,19 +630,6 @@ async fn insert_next_server_special_aliases(
         .resolved_cell(),
     );
 
-    import_map.insert_exact_alias(
-        "@opentelemetry/api",
-        // It needs to prefer the local version of @opentelemetry/api
-        ImportMapping::Alternatives(vec![
-            external_cjs_if_node(project_path.clone(), "@opentelemetry/api"),
-            external_cjs_if_node(
-                project_path.clone(),
-                "next/dist/compiled/@opentelemetry/api",
-            ),
-        ])
-        .resolved_cell(),
-    );
-
     match &ty {
         ServerContextType::Pages { .. } | ServerContextType::PagesApi { .. } => {}
         ServerContextType::PagesData { .. } => {}

filename: crates/next-core/src/next_server/context.rs
patch: @@ -47,7 +47,7 @@ use crate::{
     next_client::RuntimeEntries,
     next_config::NextConfig,
     next_font::local::NextFontLocalResolvePlugin,
-    next_import_map::get_next_server_import_map,
+    next_import_map::{get_next_edge_and_server_fallback_import_map, get_next_server_import_map},
     next_server::resolve::ExternalPredicate,
     next_shared::{
         resolve::{
@@ -139,6 +139,11 @@ pub async fn get_server_resolve_options_context(
     )
     .to_resolved()
     .await?;
+    let next_server_fallback_import_map =
+        get_next_edge_and_server_fallback_import_map(project_path.clone(), NextRuntime::NodeJs)
+            .to_resolved()
+            .await?;
+
     let foreign_code_context_condition =
         foreign_code_context_condition(next_config, project_path.clone()).await?;
     let root_dir = project_path.root().owned().await?;
@@ -319,6 +324,7 @@ pub async fn get_server_resolve_options_context(
         module: true,
         custom_conditions,
         import_map: Some(next_server_import_map),
+        fallback_import_map: Some(next_server_fallback_import_map),
         before_resolve_plugins,
         after_resolve_plugins,
         ..Default::default()

filename: test/integration/edge-runtime-dynamic-code/test/index.test.js
patch: @@ -126,9 +126,7 @@ describe.each([
                     // TODO(veil): Turbopack duplicates project path
                     '\n    at usingEval (../../test/integration/edge-runtime-dynamic-code/test/integration/edge-runtime-dynamic-code/lib/utils.js:11:16)' +
                     '\n    at handler (../../test/integration/edge-runtime-dynamic-code/test/integration/edge-runtime-dynamic-code/pages/api/route.js:13:22)' +
-                    // @opentelemetry/api internal frame. Feel free to adjust.
-                    // Not ignore-listed because we're not in an isolated app and Next.js is symlinked so it's not in node_modules
-                    '\n    at'
+                    '\n   9 | export async function usingEval() {'
                 : '\n    at usingEval (../../test/integration/edge-runtime-dynamic-code/lib/utils.js:11:18)' +
                     '\n    at handler (../../test/integration/edge-runtime-dynamic-code/pages/api/route.js:13:23)' +
                     '\n   9 | export async function usingEval() {'
@@ -192,9 +190,7 @@ describe.each([
                     // TODO(veil): Turbopack duplicates project path
                     '\n    at usingWebAssemblyCompile (../../test/integration/edge-runtime-dynamic-code/test/integration/edge-runtime-dynamic-code/lib/wasm.js:22:17)' +
                     '\n    at handler (../../test/integration/edge-runtime-dynamic-code/test/integration/edge-runtime-dynamic-code/pages/api/route.js:17:42)' +
-                    // Next.js internal frame. Feel free to adjust.
-                    // Not ignore-listed because we're not in an isolated app and Next.js is symlinked so it's not in node_modules
-                    '\n    at'
+                    '\n  20 |'
                 : '' +
                     '\n    at usingWebAssemblyCompile (../../test/integration/edge-runtime-dynamic-code/lib/wasm.js:22:23)' +
                     '\n    at handler (../../test/integration/edge-runtime-dynamic-code/pages/api/route.js:17:42)' +