Repository: vercel/next.js
Pull request: Turbopack: prevent duplicate NFT modules

Previously, you could have duplicate NFT modules (and duplicate analysis):

- user code ESM imports external `twoslash` which loads `typescript`
- user code CJS imports external `ts-morph` which loads `typescript`

Both cases would get different asset contexts, due to `externals_tracing_module_context(ExternalType::CommonJS)`  vs `externals_tracing_module_context(ExternalType::Esm)`

<img width="2560" height="856" alt="Bildschirmfoto 2025-09-30 um 11 00 18" src="https://github.com/user-attachments/assets/7604783b-6e16-4b34-9af3-d8aa65a0cff8" />

Closes PACK-5595

Changed files:
filename: crates/next-api/src/next_server_nft.rs
patch: @@ -18,7 +18,7 @@ use turbopack_core::{
     file_source::FileSource,
     output::{OutputAsset, OutputAssets},
     reference_type::{CommonJsReferenceSubType, ReferenceType},
-    resolve::{ExternalType, origin::PlainResolveOrigin, parse::Request},
+    resolve::{origin::PlainResolveOrigin, parse::Request},
     traced_asset::TracedAsset,
 };
 use turbopack_ecmascript::resolve::cjs_resolve;
@@ -169,7 +169,6 @@ impl ServerNftJsonAsset {
         let is_standalone = *self.project.next_config().is_standalone().await?;
 
         let asset_context = Vc::upcast(externals_tracing_module_context(
-            ExternalType::CommonJs,
             get_tracing_compile_time_info(),
         ));
 

filename: turbopack/crates/turbopack-ecmascript/src/references/external_module.rs
patch: @@ -4,21 +4,22 @@ use anyhow::Result;
 use serde::{Deserialize, Serialize};
 use turbo_rcstr::{RcStr, rcstr};
 use turbo_tasks::{NonLocalValue, ResolvedVc, TaskInput, TryJoinIterExt, Vc, trace::TraceRawVcs};
-use turbo_tasks_fs::{
-    FileContent, FileSystem, FileSystemPath, VirtualFileSystem, glob::Glob, rope::RopeBuilder,
-};
+use turbo_tasks_fs::{FileContent, FileSystem, VirtualFileSystem, glob::Glob, rope::RopeBuilder};
 use turbopack_core::{
     asset::{Asset, AssetContent},
     chunk::{AsyncModuleInfo, ChunkItem, ChunkType, ChunkableModule, ChunkingContext},
-    context::AssetContext,
     ident::{AssetIdent, Layer},
     module::Module,
     module_graph::ModuleGraph,
     raw_module::RawModule,
     reference::{ModuleReference, ModuleReferences, TracedModuleReference},
     reference_type::ReferenceType,
-    resolve::parse::Request,
+    resolve::{
+        origin::{ResolveOrigin, ResolveOriginExt},
+        parse::Request,
+    },
 };
+use turbopack_resolve::ecmascript::{cjs_resolve, esm_resolve};
 
 use crate::{
     EcmascriptModuleContent,
@@ -63,8 +64,7 @@ pub enum CachedExternalType {
 pub enum CachedExternalTracingMode {
     Untraced,
     Traced {
-        externals_context: ResolvedVc<Box<dyn AssetContext>>,
-        root_origin: FileSystemPath,
+        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
     },
 }
 
@@ -228,29 +228,41 @@ impl Module for CachedExternalModule {
     async fn references(&self) -> Result<Vc<ModuleReferences>> {
         Ok(match &self.analyze_mode {
             CachedExternalTracingMode::Untraced => ModuleReferences::empty(),
-            CachedExternalTracingMode::Traced {
-                externals_context,
-                root_origin,
-            } => {
-                let reference_type = match self.external_type {
+            CachedExternalTracingMode::Traced { origin } => {
+                let external_result = match self.external_type {
                     CachedExternalType::EcmaScriptViaImport => {
-                        ReferenceType::EcmaScriptModules(Default::default())
+                        esm_resolve(
+                            **origin,
+                            Request::parse_string(self.request.clone()),
+                            Default::default(),
+                            false,
+                            None,
+                        )
+                        .await?
+                        .await?
                     }
                     CachedExternalType::CommonJs | CachedExternalType::EcmaScriptViaRequire => {
-                        ReferenceType::CommonJs(Default::default())
+                        cjs_resolve(
+                            **origin,
+                            Request::parse_string(self.request.clone()),
+                            Default::default(),
+                            None,
+                            false,
+                        )
+                        .await?
+                    }
+                    CachedExternalType::Global | CachedExternalType::Script => {
+                        origin
+                            .resolve_asset(
+                                Request::parse_string(self.request.clone()),
+                                origin.resolve_options(ReferenceType::Undefined).await?,
+                                ReferenceType::Undefined,
+                            )
+                            .await?
+                            .await?
                     }
-                    _ => ReferenceType::Undefined,
                 };
 
-                let external_result = externals_context
-                    .resolve_asset(
-                        root_origin.clone(),
-                        Request::parse_string(self.request.clone()),
-                        externals_context
-                            .resolve_options(root_origin.clone(), reference_type.clone()),
-                        reference_type,
-                    )
-                    .await?;
                 let references = external_result
                     .affecting_sources
                     .iter()

filename: turbopack/crates/turbopack/src/lib.rs
patch: @@ -713,19 +713,14 @@ async fn process_default_internal(
 
 #[turbo_tasks::function]
 pub async fn externals_tracing_module_context(
-    ty: ExternalType,
     compile_time_info: Vc<CompileTimeInfo>,
 ) -> Result<Vc<ModuleAssetContext>> {
     let resolve_options = ResolveOptionsContext {
         enable_node_native_modules: true,
         emulate_environment: Some(compile_time_info.await?.environment),
         loose_errors: true,
         collect_affecting_sources: true,
-        custom_conditions: match ty {
-            ExternalType::CommonJs => vec![rcstr!("require"), rcstr!("node")],
-            ExternalType::EcmaScriptModule => vec![rcstr!("import"), rcstr!("node")],
-            ExternalType::Url | ExternalType::Global | ExternalType::Script => vec![rcstr!("node")],
-        },
+        custom_conditions: vec![rcstr!("node")],
         ..Default::default()
     };
 
@@ -869,16 +864,14 @@ impl AssetContext for ModuleAssetContext {
                                     // anyway.
 
                                     let options = options.await?;
+                                    let origin = PlainResolveOrigin::new(
+                                        Vc::upcast(externals_tracing_module_context(
+                                            *options.compile_time_info,
+                                        )),
+                                        options.tracing_root.join("_")?,
+                                    );
                                     CachedExternalTracingMode::Traced {
-                                        externals_context: ResolvedVc::upcast(
-                                            externals_tracing_module_context(
-                                                ty,
-                                                *options.compile_time_info,
-                                            )
-                                            .to_resolved()
-                                            .await?,
-                                        ),
-                                        root_origin: options.tracing_root.join("_")?,
+                                        origin: ResolvedVc::upcast(origin.to_resolved().await?),
                                     }
                                 } else {
                                     CachedExternalTracingMode::Untraced