diff --git a/packages/next-swc/crates/next-core/src/app_structure.rs b/packages/next-swc/crates/next-core/src/app_structure.rs index 19d7fff65d6e8..ffe3d2dfebcb0 100644 --- a/packages/next-swc/crates/next-core/src/app_structure.rs +++ b/packages/next-swc/crates/next-core/src/app_structure.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use indexmap::{ indexmap, map::{Entry, OccupiedEntry}, @@ -66,20 +66,6 @@ impl Components { metadata: self.metadata.clone(), } } - - fn merge(a: &Self, b: &Self) -> Self { - Self { - page: a.page.or(b.page), - layout: a.layout.or(b.layout), - error: a.error.or(b.error), - loading: a.loading.or(b.loading), - template: a.template.or(b.template), - not_found: a.not_found.or(b.not_found), - default: a.default.or(b.default), - route: a.route.or(b.route), - metadata: Metadata::merge(&a.metadata, &b.metadata), - } - } } #[turbo_tasks::value_impl] @@ -185,30 +171,13 @@ impl Metadata { twitter, open_graph, sitemap, - base_page, + base_page: _, } = self; icon.is_empty() && apple.is_empty() && twitter.is_empty() && open_graph.is_empty() && sitemap.is_none() - && base_page.is_none() - } - - fn merge(a: &Self, b: &Self) -> Self { - Self { - icon: a.icon.iter().chain(b.icon.iter()).copied().collect(), - apple: a.apple.iter().chain(b.apple.iter()).copied().collect(), - twitter: a.twitter.iter().chain(b.twitter.iter()).copied().collect(), - open_graph: a - .open_graph - .iter() - .chain(b.open_graph.iter()) - .copied() - .collect(), - sitemap: a.sitemap.or(b.sitemap), - base_page: a.base_page.as_ref().or(b.base_page.as_ref()).cloned(), - } } } @@ -431,39 +400,6 @@ pub struct LoaderTree { pub global_metadata: Vc, } -#[turbo_tasks::function] -async fn merge_loader_trees( - app_dir: Vc, - tree1: Vc, - tree2: Vc, -) -> Result> { - let tree1 = tree1.await?; - let tree2 = tree2.await?; - - let segment = if !tree1.segment.is_empty() { - tree1.segment.to_string() - } else { - tree2.segment.to_string() - }; - - let mut parallel_routes = tree1.parallel_routes.clone(); - for (key, &tree2_route) in tree2.parallel_routes.iter() { - add_parallel_route(app_dir, &mut parallel_routes, key.clone(), tree2_route).await? - } - - let components = Components::merge(&*tree1.components.await?, &*tree2.components.await?).cell(); - - Ok(LoaderTree { - page: tree1.page.clone(), - segment, - parallel_routes, - components, - // this is always the same, no need to merge it - global_metadata: tree1.global_metadata, - } - .cell()) -} - #[derive( Clone, PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, ValueDebugFormat, Debug, TaskInput, )] @@ -493,26 +429,6 @@ fn match_parallel_route(name: &str) -> Option<&str> { name.strip_prefix('@') } -async fn add_parallel_route( - app_dir: Vc, - result: &mut IndexMap>, - key: String, - loader_tree: Vc, -) -> Result<()> { - match result.entry(key) { - Entry::Occupied(mut e) => { - let value = e.get_mut(); - *value = merge_loader_trees(app_dir, *value, loader_tree) - .resolve() - .await?; - } - Entry::Vacant(e) => { - e.insert(loader_tree); - } - } - Ok(()) -} - fn conflict_issue( app_dir: Vc, e: &OccupiedEntry, @@ -562,20 +478,27 @@ async fn add_app_page( match value { Entrypoint::AppPage { page: existing_page, - .. + loader_tree: existing_loader_tree, } => { - if *existing_page != page { + // loader trees should always match for the same path as they are generated by a + // turbo tasks function + if *existing_loader_tree != loader_tree { conflict("page", existing_page); - return Ok(()); } - if let Entrypoint::AppPage { - loader_tree: value, .. + let Entrypoint::AppPage { + page: stored_page, .. } = e.get_mut() + else { + unreachable!("Entrypoint::AppPage was already matched"); + }; + + // next.js does some weird stuff when looking up routes so we have to emit the + // correct path (shortest segments, but alphabetically the last). + if page.len() < stored_page.len() + || (page.len() == stored_page.len() && page.to_string() > stored_page.to_string()) { - *value = merge_loader_trees(app_dir, *value, loader_tree) - .resolve() - .await?; + *stored_page = page; } } Entrypoint::AppRoute { @@ -704,20 +627,25 @@ fn directory_tree_to_entrypoints( ) } +/// creates the loader tree for a specific route (pathname / [AppPath]) #[turbo_tasks::function] -async fn directory_tree_to_entrypoints_internal( +async fn directory_tree_to_loader_tree( app_dir: Vc, global_metadata: Vc, directory_name: String, directory_tree: Vc, app_page: AppPage, -) -> Result> { - let mut result = IndexMap::new(); + // the page this loader tree is constructed for + for_app_path: AppPath, +) -> Result>>> { + let app_path = AppPath::from(app_page.clone()); - let directory_tree = &*directory_tree.await?; + if !for_app_path.contains(&app_path) { + return Ok(Vc::cell(None)); + } - let subdirectories = &directory_tree.subdirectories; - let components = directory_tree.components.await?; + let directory_tree = &*directory_tree.await?; + let mut components = directory_tree.components.await?.clone_value(); // Capture the current page for the metadata to calculate segment relative to // the corresponding page for the static metadata files. @@ -727,23 +655,32 @@ async fn directory_tree_to_entrypoints_internal( ..components.metadata.clone() }; */ - let components = if components.metadata.base_page.is_some() { - components - } else { - (Components { - metadata: Metadata { - base_page: Some(app_page.clone()), - ..components.metadata.clone() - }, - ..*components - }) - .cell() - .await? + components.metadata.base_page = Some(app_page.clone()); + + if app_page.is_root() && components.not_found.is_none() { + components.not_found = Some( + get_next_package(app_dir).join("dist/client/components/not-found-error.js".to_string()), + ); + } + + let mut tree = LoaderTree { + page: app_page.clone(), + segment: directory_name.clone(), + parallel_routes: IndexMap::new(), + components: components.without_leafs().cell(), + global_metadata, }; let current_level_is_parallel_route = is_parallel_route(&directory_name); - if let Some(page) = components.page { + if current_level_is_parallel_route { + tree.segment = "children".to_string(); + } + + if let Some(page) = (app_path == for_app_path) + .then_some(components.page) + .flatten() + { // When resolving metadata with corresponding module // (https://github.com/vercel/next.js/blob/aa1ee5995cdd92cc9a2236ce4b6aa2b67c9d32b2/packages/next/src/lib/metadata/resolve-metadata.ts#L340) // layout takes precedence over page (https://github.com/vercel/next.js/blob/aa1ee5995cdd92cc9a2236ce4b6aa2b67c9d32b2/packages/next/src/server/lib/app-dir-module.ts#L22) @@ -755,93 +692,157 @@ async fn directory_tree_to_entrypoints_internal( components.metadata.clone() }; - add_app_page( + tree.parallel_routes.insert( + "children".to_string(), + LoaderTree { + page: app_page.clone(), + segment: "__PAGE__".to_string(), + parallel_routes: IndexMap::new(), + components: Components { + page: Some(page), + metadata, + ..Default::default() + } + .cell(), + global_metadata, + } + .cell(), + ); + + if current_level_is_parallel_route { + tree.segment = "page$".to_string(); + } + } + + for (subdir_name, subdirectory) in &directory_tree.subdirectories { + let parallel_route_key = match_parallel_route(subdir_name); + + let mut child_app_page = app_page.clone(); + let mut illegal_path_error = None; + + // When constructing the app_page fails (e. g. due to limitations of the order), + // we only want to emit the error when there are actual pages below that + // directory. + if let Err(e) = child_app_page.push_str(subdir_name) { + illegal_path_error = Some(e); + } + + let subtree = *directory_tree_to_loader_tree( app_dir, - &mut result, - app_page.complete(PageType::Page)?, - if current_level_is_parallel_route { - LoaderTree { - page: app_page.clone(), - segment: "__PAGE__".to_string(), - parallel_routes: IndexMap::new(), - components: Components { - page: Some(page), - metadata, + global_metadata, + subdir_name.clone(), + *subdirectory, + child_app_page.clone(), + for_app_path.clone(), + ) + .await?; + + if let Some(illegal_path) = subtree.and(illegal_path_error) { + return Err(illegal_path); + } + + if let Some(subtree) = subtree { + let key = parallel_route_key.unwrap_or("children").to_string(); + tree.parallel_routes.insert(key, subtree); + } else if let Some(key) = parallel_route_key { + bail!( + "missing page or default for parallel route `{}` (page: {})", + key, + app_page + ); + } + } + + if tree.parallel_routes.is_empty() { + tree.segment = "__DEFAULT__".to_string(); + if let Some(default) = components.default { + tree.components = Components { + default: Some(default), + ..Default::default() + } + .cell(); + } else if components.layout.is_some() || current_level_is_parallel_route { + // default fallback component + tree.components = Components { + default: Some( + get_next_package(app_dir) + .join("dist/client/components/parallel-route-default.js".to_string()), + ), + ..Default::default() + } + .cell(); + } else { + return Ok(Vc::cell(None)); + } + } else if tree.parallel_routes.get("children").is_none() { + tree.parallel_routes.insert( + "children".to_string(), + LoaderTree { + page: app_page.clone(), + segment: "__DEFAULT__".to_string(), + parallel_routes: IndexMap::new(), + components: if let Some(default) = components.default { + Components { + default: Some(default), ..Default::default() } - .cell(), - global_metadata, - } - .cell() - } else { - LoaderTree { - page: app_page.clone(), - segment: directory_name.to_string(), - parallel_routes: indexmap! { - "children".to_string() => LoaderTree { - page: app_page.clone(), - segment: "__PAGE__".to_string(), - parallel_routes: IndexMap::new(), - components: Components { - page: Some(page), - metadata, - ..Default::default() - } - .cell(), - global_metadata, - } - .cell(), - }, - components: components.without_leafs().cell(), - global_metadata, - } - .cell() - }, + .cell() + } else { + // default fallback component + Components { + default: Some( + get_next_package(app_dir).join( + "dist/client/components/parallel-route-default.js".to_string(), + ), + ), + ..Default::default() + } + .cell() + }, + global_metadata, + } + .cell(), + ); + } + + Ok(Vc::cell(Some(tree.cell()))) +} + +#[turbo_tasks::function] +async fn directory_tree_to_entrypoints_internal( + app_dir: Vc, + global_metadata: Vc, + directory_name: String, + directory_tree: Vc, + app_page: AppPage, +) -> Result> { + let mut result = IndexMap::new(); + + let directory_tree_vc = directory_tree; + let directory_tree = &*directory_tree.await?; + + let subdirectories = &directory_tree.subdirectories; + let components = directory_tree.components.await?.clone_value(); + + // if let Some(_) = components.page.or(components.default) { + if components.page.is_some() { + let app_path = AppPath::from(app_page.clone()); + + let loader_tree = *directory_tree_to_loader_tree( + app_dir, + global_metadata, + directory_name.clone(), + directory_tree_vc, + app_page.clone(), + app_path, ) .await?; - } - if let Some(default) = components.default { add_app_page( app_dir, &mut result, app_page.complete(PageType::Page)?, - if current_level_is_parallel_route { - LoaderTree { - page: app_page.clone(), - segment: "__DEFAULT__".to_string(), - parallel_routes: IndexMap::new(), - components: Components { - default: Some(default), - ..Default::default() - } - .cell(), - global_metadata, - } - .cell() - } else { - LoaderTree { - page: app_page.clone(), - segment: directory_name.to_string(), - parallel_routes: indexmap! { - "children".to_string() => LoaderTree { - page: app_page.clone(), - segment: "__DEFAULT__".to_string(), - parallel_routes: IndexMap::new(), - components: Components { - default: Some(default), - ..Default::default() - } - .cell(), - global_metadata, - } - .cell(), - }, - components: components.without_leafs().cell(), - global_metadata, - } - .cell() - }, + loader_tree.context("loader tree should be created for a page/default")?, ) .await?; } @@ -905,7 +906,7 @@ async fn directory_tree_to_entrypoints_internal( if let Some(_not_found) = components.not_found { let dev_not_found_tree = LoaderTree { page: app_page.clone(), - segment: directory_name.to_string(), + segment: directory_name.clone(), parallel_routes: indexmap! { "children".to_string() => LoaderTree { page: app_page.clone(), @@ -964,17 +965,14 @@ async fn directory_tree_to_entrypoints_internal( } for (subdir_name, &subdirectory) in subdirectories.iter() { - let parallel_route_key = match_parallel_route(subdir_name); - - let mut app_page = app_page.clone(); + let mut child_app_page = app_page.clone(); let mut illegal_path = None; - if parallel_route_key.is_none() { - // When constructing the app_page fails (e. g. due to limitations of the order), - // we only want to emit the error when there are actual pages below that - // directory. - if let Err(e) = app_page.push_str(subdir_name) { - illegal_path = Some(e); - } + + // When constructing the app_page fails (e. g. due to limitations of the order), + // we only want to emit the error when there are actual pages below that + // directory. + if let Err(e) = child_app_page.push_str(subdir_name) { + illegal_path = Some(e); } let map = directory_tree_to_entrypoints_internal( @@ -982,7 +980,7 @@ async fn directory_tree_to_entrypoints_internal( global_metadata, subdir_name.to_string(), subdirectory, - app_page.clone(), + child_app_page.clone(), ) .await?; @@ -996,24 +994,27 @@ async fn directory_tree_to_entrypoints_internal( match *entrypoint { Entrypoint::AppPage { ref page, - loader_tree, + loader_tree: _, } => { - if current_level_is_parallel_route { - add_app_page(app_dir, &mut result, page.clone(), loader_tree).await?; - } else { - let key = parallel_route_key.unwrap_or("children").to_string(); - let child_loader_tree = LoaderTree { - page: app_page.clone(), - segment: directory_name.to_string(), - parallel_routes: indexmap! { - key => loader_tree, - }, - components: components.without_leafs().cell(), - global_metadata, - } - .cell(); - add_app_page(app_dir, &mut result, page.clone(), child_loader_tree).await?; - } + let app_path = AppPath::from(page.clone()); + + let loader_tree = *directory_tree_to_loader_tree( + app_dir, + global_metadata, + directory_name.clone(), + directory_tree_vc, + app_page.clone(), + app_path, + ) + .await?; + + add_app_page( + app_dir, + &mut result, + page.clone(), + loader_tree.context("loader tree should be created for a page/default")?, + ) + .await?; } Entrypoint::AppRoute { ref page, path } => { add_app_route(app_dir, &mut result, page.clone(), path); diff --git a/packages/next-swc/crates/next-core/src/next_app/mod.rs b/packages/next-swc/crates/next-core/src/next_app/mod.rs index 58ffdcfb4ba18..2a575c32f7745 100644 --- a/packages/next-swc/crates/next-core/src/next_app/mod.rs +++ b/packages/next-swc/crates/next-core/src/next_app/mod.rs @@ -296,6 +296,16 @@ impl AppPath { ) }) } + + pub fn contains(&self, other: &AppPath) -> bool { + for (i, segment) in other.0.iter().enumerate() { + if self.0.get(i) != Some(segment) { + return false; + } + } + + true + } } impl Deref for AppPath { diff --git a/test/turbopack-tests-manifest.json b/test/turbopack-tests-manifest.json index 876b61f6d91a4..7f65a984e578f 100644 --- a/test/turbopack-tests-manifest.json +++ b/test/turbopack-tests-manifest.json @@ -3618,40 +3618,40 @@ "test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts": { "passed": [ "parallel-routes-and-interception parallel routes should apply the catch-all route to the parallel route if no matching route is found", - "parallel-routes-and-interception parallel routes should match parallel routes", - "parallel-routes-and-interception parallel routes should render nested parallel routes", - "parallel-routes-and-interception route intercepting should re-render the layout on the server when it had a default child route", - "parallel-routes-and-interception route intercepting should render intercepted route", - "parallel-routes-and-interception route intercepting should render intercepted route from a nested route", - "parallel-routes-and-interception route intercepting should support intercepting with beforeFiles rewrites", - "parallel-routes-and-interception route intercepting with dynamic catch-all routes should render intercepted route", - "parallel-routes-and-interception route intercepting with dynamic optional catch-all routes should render intercepted route", - "parallel-routes-and-interception route intercepting with dynamic routes should render intercepted route" - ], - "failed": [ "parallel-routes-and-interception parallel routes should display all parallel route params with useParams", "parallel-routes-and-interception parallel routes should match parallel routes in route groups", + "parallel-routes-and-interception parallel routes should match parallel routes", "parallel-routes-and-interception parallel routes should navigate with a link with prefetch=false", "parallel-routes-and-interception parallel routes should only scroll to the parallel route that was navigated to", + "parallel-routes-and-interception parallel routes should render nested parallel routes", "parallel-routes-and-interception parallel routes should support layout files in parallel routes", "parallel-routes-and-interception parallel routes should support nested parallel routes", "parallel-routes-and-interception parallel routes should support parallel route tab bars", "parallel-routes-and-interception parallel routes should support parallel routes with no page component", "parallel-routes-and-interception parallel routes should throw a 404 when no matching parallel route is found", - "parallel-routes-and-interception parallel routes should throw an error when a route groups causes a conflict with a parallel segment", + "parallel-routes-and-interception route intercepting should re-render the layout on the server when it had a default child route", "parallel-routes-and-interception route intercepting should render an intercepted route at the top level from a nested path", "parallel-routes-and-interception route intercepting should render an intercepted route from a slot", - "parallel-routes-and-interception route intercepting should render modal when paired with parallel routes" + "parallel-routes-and-interception route intercepting should render intercepted route from a nested route", + "parallel-routes-and-interception route intercepting should render intercepted route", + "parallel-routes-and-interception route intercepting should render modal when paired with parallel routes", + "parallel-routes-and-interception route intercepting should support intercepting with beforeFiles rewrites", + "parallel-routes-and-interception route intercepting with dynamic catch-all routes should render intercepted route", + "parallel-routes-and-interception route intercepting with dynamic optional catch-all routes should render intercepted route", + "parallel-routes-and-interception route intercepting with dynamic routes should render intercepted route" + ], + "failed": [ + "parallel-routes-and-interception parallel routes should throw an error when a route groups causes a conflict with a parallel segment" ], "pending": [], "flakey": [], "runtimeError": false }, "test/e2e/app-dir/parallel-routes-not-found/parallel-routes-not-found.test.ts": { - "passed": [], - "failed": [ + "passed": [ "parallel-routes-and-interception should not render the @children slot when the @slot is not found" ], + "failed": [], "pending": [], "flakey": [], "runtimeError": false