Repository: vercel/next.js
Pull request: Turbopack: fixup route hashes generation

Previously, it tried to compute the hash of some pages router endpoints, even they weren't in the graph (because there is no `pages` folder)

Changed files:
filename: crates/next-api/src/project.rs
patch: @@ -83,7 +83,10 @@ use crate::{
     instrumentation::InstrumentationEndpoint,
     middleware::MiddlewareEndpoint,
     pages::PagesProject,
-    route::{Endpoint, EndpointGroup, EndpointGroupKey, EndpointGroups, Endpoints, Route},
+    route::{
+        Endpoint, EndpointGroup, EndpointGroupEntry, EndpointGroupKey, EndpointGroups, Endpoints,
+        Route,
+    },
     versioned_content_map::VersionedContentMap,
 };
 
@@ -936,9 +939,18 @@ impl Project {
                         endpoint_groups.push((
                             EndpointGroupKey::Route(key.clone()),
                             EndpointGroup {
-                                primary: vec![*html_endpoint],
+                                primary: vec![EndpointGroupEntry {
+                                    endpoint: *html_endpoint,
+                                    sub_name: None,
+                                }],
                                 // This only exists in development mode for HMR
-                                additional: data_endpoint.iter().copied().collect(),
+                                additional: data_endpoint
+                                    .iter()
+                                    .map(|endpoint| EndpointGroupEntry {
+                                        endpoint: *endpoint,
+                                        sub_name: None,
+                                    })
+                                    .collect(),
                             },
                         ));
                         add_pages_entries = true;
@@ -957,7 +969,13 @@ impl Project {
                     endpoint_groups.push((
                         EndpointGroupKey::Route(key.clone()),
                         EndpointGroup {
-                            primary: page_routes.iter().map(|r| r.html_endpoint).collect(),
+                            primary: page_routes
+                                .iter()
+                                .map(|r| EndpointGroupEntry {
+                                    endpoint: r.html_endpoint,
+                                    sub_name: Some(r.original_name.clone()),
+                                })
+                                .collect(),
                             additional: Vec::new(),
                         },
                     ));
@@ -999,11 +1017,11 @@ impl Project {
     pub async fn get_all_endpoints(self: Vc<Self>, app_dir_only: bool) -> Result<Vc<Endpoints>> {
         let mut endpoints = Vec::new();
         for (_key, group) in self.get_all_endpoint_groups(app_dir_only).await?.iter() {
-            for &endpoint in group.primary.iter() {
-                endpoints.push(endpoint);
+            for entry in group.primary.iter() {
+                endpoints.push(entry.endpoint);
             }
-            for &endpoint in group.additional.iter() {
-                endpoints.push(endpoint);
+            for entry in group.additional.iter() {
+                endpoints.push(entry.endpoint);
             }
         }
 

filename: crates/next-api/src/route.rs
patch: @@ -95,6 +95,20 @@ pub enum EndpointGroupKey {
     Route(RcStr),
 }
 
+impl EndpointGroupKey {
+    pub fn as_str(&self) -> &str {
+        match self {
+            EndpointGroupKey::Instrumentation => "instrumentation",
+            EndpointGroupKey::InstrumentationEdge => "instrumentation-edge",
+            EndpointGroupKey::Middleware => "middleware",
+            EndpointGroupKey::PagesError => "_error",
+            EndpointGroupKey::PagesApp => "_app",
+            EndpointGroupKey::PagesDocument => "_document",
+            EndpointGroupKey::Route(route) => route,
+        }
+    }
+}
+
 impl Display for EndpointGroupKey {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
@@ -104,11 +118,27 @@ impl Display for EndpointGroupKey {
             EndpointGroupKey::PagesError => write!(f, "_error"),
             EndpointGroupKey::PagesApp => write!(f, "_app"),
             EndpointGroupKey::PagesDocument => write!(f, "_document"),
-            EndpointGroupKey::Route(route) => write!(f, "/{}", route),
+            EndpointGroupKey::Route(route) => write!(f, "{}", route),
         }
     }
 }
 
+#[derive(
+    TraceRawVcs,
+    Serialize,
+    Deserialize,
+    PartialEq,
+    Eq,
+    ValueDebugFormat,
+    Clone,
+    Debug,
+    NonLocalValue,
+)]
+pub struct EndpointGroupEntry {
+    pub endpoint: ResolvedVc<Box<dyn Endpoint>>,
+    pub sub_name: Option<RcStr>,
+}
+
 #[derive(
     TraceRawVcs,
     Serialize,
@@ -121,24 +151,37 @@ impl Display for EndpointGroupKey {
     NonLocalValue,
 )]
 pub struct EndpointGroup {
-    pub primary: Vec<ResolvedVc<Box<dyn Endpoint>>>,
-    pub additional: Vec<ResolvedVc<Box<dyn Endpoint>>>,
+    pub primary: Vec<EndpointGroupEntry>,
+    pub additional: Vec<EndpointGroupEntry>,
 }
 
 impl EndpointGroup {
     pub fn from(endpoint: ResolvedVc<Box<dyn Endpoint>>) -> Self {
         Self {
-            primary: vec![endpoint],
+            primary: vec![EndpointGroupEntry {
+                endpoint,
+                sub_name: None,
+            }],
             additional: vec![],
         }
     }
 
     pub fn output_assets(&self) -> Vc<OutputAssets> {
-        output_of_endpoints(self.primary.iter().map(|endpoint| **endpoint).collect())
+        output_of_endpoints(
+            self.primary
+                .iter()
+                .map(|endpoint| *endpoint.endpoint)
+                .collect(),
+        )
     }
 
     pub fn module_graphs(&self) -> Vc<ModuleGraphs> {
-        module_graphs_of_endpoints(self.primary.iter().map(|endpoint| **endpoint).collect())
+        module_graphs_of_endpoints(
+            self.primary
+                .iter()
+                .map(|endpoint| *endpoint.endpoint)
+                .collect(),
+        )
     }
 }
 

filename: crates/next-api/src/routes_hashes_manifest.rs
patch: @@ -1,5 +1,6 @@
 use anyhow::Result;
 use serde::Serialize;
+use turbo_rcstr::RcStr;
 use turbo_tasks::{FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc};
 use turbo_tasks_fs::{FileContent, FileSystemPath};
 use turbo_tasks_hash::{DeterministicHash, Xxh3Hash64Hasher};
@@ -15,7 +16,7 @@ use turbopack_core::{
 
 use crate::{
     project::Project,
-    route::{AppPageRoute, Endpoint, Route},
+    route::{Endpoint, EndpointGroup},
 };
 
 #[turbo_tasks::value(shared)]
@@ -25,7 +26,7 @@ pub struct EndpointHashes {
 }
 
 impl EndpointHashes {
-    pub fn merge<'l>(iterator: impl Iterator<Item = (&'l str, &'l EndpointHashes)>) -> Self {
+    pub fn merge<'l>(iterator: impl Iterator<Item = (Option<RcStr>, &'l EndpointHashes)>) -> Self {
         let mut sources_hasher = Xxh3Hash64Hasher::new();
         let mut outputs_hasher = Xxh3Hash64Hasher::new();
 
@@ -151,138 +152,49 @@ impl RoutesHashesManifestAsset {
 impl Asset for RoutesHashesManifestAsset {
     #[turbo_tasks::function]
     async fn content(&self) -> Result<Vc<AssetContent>> {
-        let entrypoints = self.project.entrypoints().await?;
         let module_graphs = self.project.whole_app_module_graphs().await?;
         let base_module_graph = *module_graphs.base;
         let full_module_graph = *module_graphs.full;
 
         let mut entrypoint_hashes = FxIndexMap::default();
 
-        if let Some(instrumentation) = &entrypoints.instrumentation {
-            entrypoint_hashes.insert(
-                "instrumentation",
-                endpoint_hashes(
-                    base_module_graph,
-                    full_module_graph,
-                    *instrumentation.node_js,
-                ),
-            );
-            entrypoint_hashes.insert(
-                "edgeInstrumentation",
-                endpoint_hashes(base_module_graph, full_module_graph, *instrumentation.edge),
-            );
-        }
-        if let Some(middleware) = &entrypoints.middleware {
-            entrypoint_hashes.insert(
-                "middleware",
-                endpoint_hashes(base_module_graph, full_module_graph, *middleware.endpoint),
-            );
-        }
-        entrypoint_hashes.insert(
-            "_document",
-            endpoint_hashes(
-                base_module_graph,
-                full_module_graph,
-                *entrypoints.pages_document_endpoint,
-            ),
-        );
-        entrypoint_hashes.insert(
-            "_app",
-            endpoint_hashes(
-                base_module_graph,
-                full_module_graph,
-                *entrypoints.pages_app_endpoint,
-            ),
-        );
-        entrypoint_hashes.insert(
-            "_error",
-            endpoint_hashes(
-                base_module_graph,
-                full_module_graph,
-                *entrypoints.pages_error_endpoint,
-            ),
-        );
-
-        for (key, route) in entrypoints.routes.iter() {
-            match route {
-                Route::Page {
-                    html_endpoint,
-                    // Only for dev
-                    data_endpoint: _,
-                } => {
-                    entrypoint_hashes.insert(
-                        key,
-                        endpoint_hashes(base_module_graph, full_module_graph, **html_endpoint),
-                    );
-                }
-                Route::PageApi { endpoint } => {
-                    entrypoint_hashes.insert(
-                        key,
-                        endpoint_hashes(base_module_graph, full_module_graph, **endpoint),
-                    );
-                }
-                Route::AppPage(pages) => {
-                    if pages.len() == 1 {
-                        entrypoint_hashes.insert(
-                            key,
-                            endpoint_hashes(
-                                base_module_graph,
-                                full_module_graph,
-                                *pages[0].html_endpoint,
-                            ),
-                        );
-                    } else {
-                        let hashes = pages
-                            .iter()
-                            .map(
-                                |&AppPageRoute {
-                                     original_name: _,
-                                     html_endpoint,
-                                     // Only for dev
-                                     rsc_endpoint: _,
-                                 }| {
-                                    endpoint_hashes(
-                                        base_module_graph,
-                                        full_module_graph,
-                                        *html_endpoint,
-                                    )
-                                },
-                            )
-                            .try_join()
-                            .await?;
-                        let hashes = EndpointHashes::merge(
-                            pages
-                                .iter()
-                                .map(|page| page.original_name.as_str())
-                                .zip(hashes.iter())
-                                .map(|(k, v)| (k, &**v)),
-                        )
-                        .cell();
-                        entrypoint_hashes.insert(key, hashes);
-                    }
-                }
-                Route::AppRoute {
-                    original_name: _,
-                    endpoint,
-                } => {
-                    entrypoint_hashes.insert(
-                        key,
-                        endpoint_hashes(base_module_graph, full_module_graph, **endpoint),
-                    );
-                }
-                Route::Conflict => {}
+        let entrypoint_groups = self.project.get_all_endpoint_groups(false).await?;
+
+        for (key, EndpointGroup { primary, .. }) in entrypoint_groups {
+            if let &[entry] = &primary.as_slice() {
+                entrypoint_hashes.insert(
+                    key.as_str(),
+                    endpoint_hashes(base_module_graph, full_module_graph, *entry.endpoint),
+                );
+            } else {
+                let hashes = primary
+                    .iter()
+                    .map(|entry| {
+                        endpoint_hashes(base_module_graph, full_module_graph, *entry.endpoint)
+                    })
+                    .try_join()
+                    .await?;
+                let hashes = EndpointHashes::merge(
+                    primary
+                        .iter()
+                        .map(|page| page.sub_name.clone())
+                        .zip(hashes.iter())
+                        .map(|(k, v)| (k, &**v)),
+                )
+                .cell();
+                entrypoint_hashes.insert(key.as_str(), hashes);
             }
         }
 
         let entrypoint_hashes_values = entrypoint_hashes.values().copied().try_join().await?;
 
         let manifest = serde_json::to_string_pretty(&RoutesHashesManifest {
             routes: entrypoint_hashes
-                .keys()
-                .zip(entrypoint_hashes_values.iter())
+                .into_keys()
+                .zip(entrypoint_hashes_values.into_iter())
                 .map(|(k, v)| {
                     (
-                        *k,
+                        k,
                         EndpointHashStrings {
                             sources_hash: format!("{:016x}", v.sources_hash),
                             outputs_hash: format!("{:016x}", v.outputs_hash),