Repository: vercel/next.js
Pull request: Turbopack: add webpack loader names to traces

When examining the long trace this morning, it wasn't immediately clear what was running in the shell-out to Node.
This doesn't fully fix the issue, but it makes it clearer which webpack loader is running, similar to how the PostCSS loader lists the name of the css file it's parsing.

In this trace, the next config contains:
```
  turbopack: {
    rules: {
      "**/*.{js,jsx,ts,tsx}": {
        loaders: ["./loader_test.js"],
      },
    },
  },
```

And the trace renders as:

<img width="995" height="553" alt="Screenshot 2025-12-15 at 11 22 39 PM" src="https://github.com/user-attachments/assets/b0936e2e-f9d5-45fc-8a1c-2f98b1aa38b6" />


Changed files:
filename: turbopack/crates/turbopack-core/src/loader.rs
patch: @@ -67,3 +67,18 @@ impl TaskInput for ResolvedWebpackLoaderItem {
 #[derive(Debug, Clone)]
 #[turbo_tasks::value(shared, transparent)]
 pub struct WebpackLoaderItems(pub Vec<WebpackLoaderItem>);
+
+impl std::fmt::Display for WebpackLoaderItems {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let mut iter = self.0.iter();
+
+        if let Some(first) = iter.next() {
+            write!(f, "{}", first.loader)?;
+            for remaining in iter {
+                write!(f, ", {}", remaining.loader)?;
+            }
+        }
+
+        Ok(())
+    }
+}

filename: turbopack/crates/turbopack-node/src/transforms/webpack.rs
patch: @@ -8,9 +8,10 @@ use futures::try_join;
 use serde::{Deserialize, Serialize};
 use serde_json::{Map as JsonMap, Value as JsonValue, json};
 use serde_with::serde_as;
+use tracing::Instrument;
 use turbo_rcstr::{RcStr, rcstr};
 use turbo_tasks::{
-    Completion, OperationVc, ResolvedVc, TaskInput, TryJoinIterExt, ValueToString, Vc,
+    Completion, OperationVc, ReadRef, ResolvedVc, TaskInput, TryJoinIterExt, ValueToString, Vc,
     trace::TraceRawVcs,
 };
 use turbo_tasks_env::ProcessEnv;
@@ -196,122 +197,136 @@ impl WebpackLoadersProcessedAsset {
     #[turbo_tasks::function]
     async fn process(&self) -> Result<Vc<ProcessWebpackLoadersResult>> {
         let transform = self.transform.await?;
+        let loaders = transform.loaders.await?;
 
-        let ExecutionContext {
-            project_path,
-            chunking_context,
-            env,
-        } = &*transform.execution_context.await?;
-        let source_content = self.source.content();
-        let AssetContent::File(file) = *source_content.await? else {
-            bail!("Webpack Loaders transform only support transforming files");
-        };
-        let FileContent::Content(file_content) = &*file.await? else {
-            return Ok(ProcessWebpackLoadersResult {
-                content: AssetContent::File(FileContent::NotFound.resolved_cell()).resolved_cell(),
-                assets: Vec::new(),
-                source_map: FileContent::NotFound.resolved_cell(),
-            }
-            .cell());
-        };
-
-        // If the content is not a valid string (e.g. binary file), handle the error and pass a
-        // Buffer to Webpack instead of a Base64 string so the build process doesn't crash.
-        let content: JsonValue = match file_content.content().to_str() {
-            Ok(utf8_str) => utf8_str.to_string().into(),
-            Err(_) => JsonValue::Object(JsonMap::from_iter(std::iter::once((
-                "binary".to_string(),
-                JsonValue::from(
-                    base64::engine::general_purpose::STANDARD
-                        .encode(file_content.content().to_bytes()),
-                ),
-            )))),
-        };
-        let evaluate_context = transform.evaluate_context;
+        let webpack_span = tracing::info_span!(
+            "webpack loader",
+            name = display(ReadRef::<WebpackLoaderItems>::as_raw_ref(&loaders))
+        );
+
+        async {
+            let ExecutionContext {
+                project_path,
+                chunking_context,
+                env,
+            } = &*transform.execution_context.await?;
+            let source_content = self.source.content();
+            let AssetContent::File(file) = *source_content.await? else {
+                bail!("Webpack Loaders transform only support transforming files");
+            };
+            let FileContent::Content(file_content) = &*file.await? else {
+                return Ok(ProcessWebpackLoadersResult {
+                    content: AssetContent::File(FileContent::NotFound.resolved_cell())
+                        .resolved_cell(),
+                    assets: Vec::new(),
+                    source_map: FileContent::NotFound.resolved_cell(),
+                }
+                .cell());
+            };
 
-        let webpack_loaders_executor = webpack_loaders_executor(*evaluate_context).module();
+            // If the content is not a valid string (e.g. binary file), handle the error and pass a
+            // Buffer to Webpack instead of a Base64 string so the build process doesn't crash.
+            let content: JsonValue = match file_content.content().to_str() {
+                Ok(utf8_str) => utf8_str.to_string().into(),
+                Err(_) => JsonValue::Object(JsonMap::from_iter(std::iter::once((
+                    "binary".to_string(),
+                    JsonValue::from(
+                        base64::engine::general_purpose::STANDARD
+                            .encode(file_content.content().to_bytes()),
+                    ),
+                )))),
+            };
+            let evaluate_context = transform.evaluate_context;
+
+            let webpack_loaders_executor = webpack_loaders_executor(*evaluate_context).module();
+
+            let entries = get_evaluate_entries(webpack_loaders_executor, *evaluate_context, None)
+                .to_resolved()
+                .await?;
 
-        let entries = get_evaluate_entries(webpack_loaders_executor, *evaluate_context, None)
+            let module_graph = ModuleGraph::from_single_graph(SingleModuleGraph::new_with_entries(
+                entries.graph_entries().to_resolved().await?,
+                false,
+                false,
+            ))
+            .connect()
             .to_resolved()
             .await?;
 
-        let module_graph = ModuleGraph::from_single_graph(SingleModuleGraph::new_with_entries(
-            entries.graph_entries().to_resolved().await?,
-            false,
-            false,
-        ))
-        .connect()
-        .to_resolved()
-        .await?;
-
-        let resource_fs_path = self.source.ident().path().await?;
-        let Some(resource_path) = project_path.get_relative_path_to(&resource_fs_path) else {
-            bail!(format!(
-                "Resource path \"{}\" need to be on project filesystem \"{}\"",
-                resource_fs_path, project_path
-            ));
-        };
-        let loaders = transform.loaders.await?;
-        let config_value = evaluate_webpack_loader(WebpackLoaderContext {
-            entries,
-            cwd: project_path.clone(),
-            env: *env,
-            context_source_for_issue: self.source,
-            chunking_context: *chunking_context,
-            module_graph,
-            resolve_options_context: Some(transform.resolve_options_context),
-            args: vec![
-                ResolvedVc::cell(content),
-                // We need to pass the query string to the loader
-                ResolvedVc::cell(resource_path.to_string().into()),
-                ResolvedVc::cell(self.source.ident().await?.query.to_string().into()),
-                ResolvedVc::cell(json!(*loaders)),
-                ResolvedVc::cell(transform.source_maps.into()),
-            ],
-            additional_invalidation: Completion::immutable().to_resolved().await?,
-        })
-        .await?;
-
-        let Some(val) = &*config_value else {
-            // An error happened, which has already been converted into an issue.
-            return Ok(ProcessWebpackLoadersResult {
-                content: AssetContent::File(FileContent::NotFound.resolved_cell()).resolved_cell(),
-                assets: Vec::new(),
-                source_map: FileContent::NotFound.resolved_cell(),
-            }
-            .cell());
-        };
-        let processed: WebpackLoadersProcessingResult = parse_json_with_source_context(val)
-            .context("Unable to deserializate response from webpack loaders transform operation")?;
-
-        // handle SourceMap
-        let source_map = if !transform.source_maps {
-            None
-        } else {
-            processed
-                .map
-                .map(|source_map| Rope::from(source_map.into_owned()))
-        };
-        let source_map = resolve_source_map_sources(source_map.as_ref(), &resource_fs_path).await?;
-
-        let file = match processed.source {
-            Either::Left(str) => File::from(str),
-            Either::Right(bytes) => File::from(bytes.binary),
-        };
-        let assets = emitted_assets_to_virtual_sources(processed.assets).await?;
-
-        let content =
-            AssetContent::File(FileContent::Content(file).resolved_cell()).resolved_cell();
-        Ok(ProcessWebpackLoadersResult {
-            content,
-            assets,
-            source_map: if let Some(source_map) = source_map {
-                FileContent::Content(File::from(source_map)).resolved_cell()
+            let resource_fs_path = self.source.ident().path().await?;
+            let Some(resource_path) = project_path.get_relative_path_to(&resource_fs_path) else {
+                bail!(format!(
+                    "Resource path \"{}\" need to be on project filesystem \"{}\"",
+                    resource_fs_path, project_path
+                ));
+            };
+            let config_value = evaluate_webpack_loader(WebpackLoaderContext {
+                entries,
+                cwd: project_path.clone(),
+                env: *env,
+                context_source_for_issue: self.source,
+                chunking_context: *chunking_context,
+                module_graph,
+                resolve_options_context: Some(transform.resolve_options_context),
+                args: vec![
+                    ResolvedVc::cell(content),
+                    // We need to pass the query string to the loader
+                    ResolvedVc::cell(resource_path.to_string().into()),
+                    ResolvedVc::cell(self.source.ident().await?.query.to_string().into()),
+                    ResolvedVc::cell(json!(*loaders)),
+                    ResolvedVc::cell(transform.source_maps.into()),
+                ],
+                additional_invalidation: Completion::immutable().to_resolved().await?,
+            })
+            .await?;
+
+            let Some(val) = &*config_value else {
+                // An error happened, which has already been converted into an issue.
+                return Ok(ProcessWebpackLoadersResult {
+                    content: AssetContent::File(FileContent::NotFound.resolved_cell())
+                        .resolved_cell(),
+                    assets: Vec::new(),
+                    source_map: FileContent::NotFound.resolved_cell(),
+                }
+                .cell());
+            };
+            let processed: WebpackLoadersProcessingResult = parse_json_with_source_context(val)
+                .context(
+                    "Unable to deserializate response from webpack loaders transform operation",
+                )?;
+
+            // handle SourceMap
+            let source_map = if !transform.source_maps {
+                None
             } else {
-                FileContent::NotFound.resolved_cell()
-            },
+                processed
+                    .map
+                    .map(|source_map| Rope::from(source_map.into_owned()))
+            };
+            let source_map =
+                resolve_source_map_sources(source_map.as_ref(), &resource_fs_path).await?;
+
+            let file = match processed.source {
+                Either::Left(str) => File::from(str),
+                Either::Right(bytes) => File::from(bytes.binary),
+            };
+            let assets = emitted_assets_to_virtual_sources(processed.assets).await?;
+
+            let content =
+                AssetContent::File(FileContent::Content(file).resolved_cell()).resolved_cell();
+            Ok(ProcessWebpackLoadersResult {
+                content,
+                assets,
+                source_map: if let Some(source_map) = source_map {
+                    FileContent::Content(File::from(source_map)).resolved_cell()
+                } else {
+                    FileContent::NotFound.resolved_cell()
+                },
+            }
+            .cell())
         }
-        .cell())
+        .instrument(webpack_span)
+        .await
     }
 }