Repository: vercel/next.js
Pull request: bundle-analyzer: merge _app route modules into current route visualization

This:

- Fetches `data/_app/analyze.data` alongside the current route's data
- Merges `_app` modules into the current route's `AnalyzeData` before visualization using a `mergeAnalyzeData()`, which merges the two datasets, reindexing the additional ones and recreating the binary data format expected by the treemap.

## Test plan

- [x] Add a large dependency (e.g. `moment`) in `pages/_app.tsx`
- [x] Run `next experimental-analyze`
- [x] Open the bundle analyzer and select a pages page
- [x] Verify moment appears in the treemap
- [x] Verify App Router projects (no `_app`) work without errors

Changed files:
filename: apps/bundle-analyzer/lib/analyze-data.ts
patch: @@ -477,9 +477,4 @@ export class AnalyzeData {
       'node_modules/next/dist/build/polyfills/polyfill-nomodule.js'
     )
   }
-
-  // Get the raw header for debugging
-  getRawAnalyzeHeader(): AnalyzeDataHeader {
-    return this.analyzeHeader
-  }
 }

filename: crates/next-api/src/analyze.rs
patch: @@ -351,6 +351,19 @@ impl ModulesDataBuilder {
     }
 }
 
+/// Merges two sets of output assets into one. Used to combine per-route output
+/// assets with shared assets (e.g. `_app`, `_document`) at report generation time.
+#[turbo_tasks::function]
+pub async fn combine_output_assets(
+    primary: Vc<OutputAssets>,
+    extra: Vc<OutputAssets>,
+) -> Result<Vc<OutputAssets>> {
+    let mut combined: Vec<ResolvedVc<Box<dyn OutputAsset>>> =
+        primary.await?.iter().copied().collect();
+    combined.extend(extra.await?.iter().copied());
+    Ok(Vc::cell(combined))
+}
+
 #[turbo_tasks::function]
 pub async fn analyze_output_assets(output_assets: Vc<OutputAssets>) -> Result<Vc<FileContent>> {
     let output_assets = all_assets_from_entries(output_assets);

filename: crates/next-napi-bindings/src/next_api/analyze.rs
patch: @@ -2,11 +2,16 @@ use std::{iter::once, sync::Arc};
 
 use anyhow::Result;
 use next_api::{
-    analyze::{AnalyzeDataOutputAsset, ModulesDataOutputAsset},
+    analyze::{AnalyzeDataOutputAsset, ModulesDataOutputAsset, combine_output_assets},
     project::ProjectContainer,
+    route::EndpointGroupKey,
 };
 use turbo_tasks::{Effects, ReadRef, ResolvedVc, TryJoinIterExt, Vc};
-use turbopack_core::{diagnostics::PlainDiagnostic, issue::PlainIssue, output::OutputAssets};
+use turbopack_core::{
+    diagnostics::PlainDiagnostic,
+    issue::PlainIssue,
+    output::{OutputAsset, OutputAssets},
+};
 
 use crate::next_api::utils::strongly_consistent_catch_collectables;
 
@@ -67,12 +72,37 @@ async fn get_analyze_data_operation(
         .join("diagnostics/analyze/data")?;
     let whole_app_module_graphs = project.whole_app_module_graphs();
     let analyze_output_root = &analyze_output_root;
-    let analyze_data = project
-        .get_all_endpoint_groups(app_dir_only)
-        .await?
+    let endpoint_groups = project.get_all_endpoint_groups(app_dir_only).await?;
+
+    // Collect output assets from _app and _document to merge into each route's
+    // analyze.data so their modules are visible in every route's treemap.
+    let mut combined_output_assets: Vec<ResolvedVc<Box<dyn OutputAsset>>> = vec![];
+    for (key, endpoint_group) in endpoint_groups.iter() {
+        if matches!(
+            key,
+            EndpointGroupKey::PagesApp | EndpointGroupKey::PagesDocument
+        ) {
+            combined_output_assets.extend(endpoint_group.output_assets().await?.iter().copied());
+        }
+    }
+
+    let has_combined = !combined_output_assets.is_empty();
+    let combined_vc = Vc::cell(combined_output_assets);
+
+    let analyze_data = endpoint_groups
         .iter()
         .map(|(key, endpoint_group)| async move {
-            let output_assets = endpoint_group.output_assets();
+            let output_assets = if has_combined
+                && !matches!(
+                    key,
+                    EndpointGroupKey::PagesApp | EndpointGroupKey::PagesDocument
+                ) {
+                // Combine route output assets with _app and _document output assets so
+                // the generated analyze.data already includes their modules.
+                combine_output_assets(endpoint_group.output_assets(), combined_vc)
+            } else {
+                endpoint_group.output_assets()
+            };
             let analyze_data = AnalyzeDataOutputAsset::new(
                 analyze_output_root
                     .join(&key.to_string())?