feat(turbopack): Check for duplicate parallel routes (#64181)

### What?

Add a validation for parallel routes with the same path.

This PR enables
`test/e2e/app-dir/conflicting-page-segments/conflicting-page-segments.test.ts`

### Why?

To match the behavior in the default mode.

### How?

Closes PACK-2912
This commit is contained in:
Donny/강동윤 2024-04-24 04:11:41 +09:00 committed by GitHub
parent 0cd93b3a19
commit 671ac35614
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 94 additions and 6 deletions

View file

@ -1,11 +1,13 @@
use std::collections::BTreeMap;
use anyhow::{bail, Context, Result};
use async_recursion::async_recursion;
use indexmap::{
indexmap,
map::{Entry, OccupiedEntry},
IndexMap,
};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use tracing::Instrument;
use turbo_tasks::{
@ -25,7 +27,7 @@ use crate::{
match_global_metadata_file, match_local_metadata_file, normalize_metadata_route,
GlobalMetadataFileMatch, MetadataFileMatch,
},
AppPage, AppPath, PageType,
AppPage, AppPath, PageSegment, PageType,
},
next_config::NextConfig,
next_import_map::get_next_package,
@ -709,6 +711,85 @@ fn directory_tree_to_entrypoints(
)
}
#[turbo_tasks::value]
struct DuplicateParallelRouteIssue {
app_dir: Vc<FileSystemPath>,
page: AppPage,
}
#[turbo_tasks::value_impl]
impl Issue for DuplicateParallelRouteIssue {
#[turbo_tasks::function]
async fn file_path(self: Vc<Self>) -> Result<Vc<FileSystemPath>> {
let this = self.await?;
Ok(this.app_dir.join(this.page.to_string()))
}
#[turbo_tasks::function]
fn stage(self: Vc<Self>) -> Vc<IssueStage> {
IssueStage::ProcessModule.cell()
}
#[turbo_tasks::function]
fn title(self: Vc<Self>) -> Vc<StyledString> {
StyledString::Text(
"You cannot have two parallel pages that resolve to the same path.".to_string(),
)
.cell()
}
}
#[async_recursion]
async fn page_path_except_parallel(loader_tree: Vc<LoaderTree>) -> Result<Option<AppPage>> {
let loader_tree = loader_tree.await?;
if loader_tree.page.iter().any(|v| {
matches!(
v,
PageSegment::CatchAll(..)
| PageSegment::OptionalCatchAll(..)
| PageSegment::Parallel(..)
)
}) {
return Ok(None);
}
if loader_tree.components.await?.page.is_some() {
return Ok(Some(loader_tree.page.clone()));
}
if let Some(children) = loader_tree.parallel_routes.get("children") {
return page_path_except_parallel(*children).await;
}
Ok(None)
}
async fn check_duplicate(
duplicate: &mut FxHashMap<AppPath, AppPage>,
loader_tree_vc: Vc<LoaderTree>,
app_dir: Vc<FileSystemPath>,
) -> Result<()> {
let loader_tree = loader_tree_vc.await?;
let page_path = page_path_except_parallel(loader_tree_vc).await?;
if let Some(page_path) = page_path {
if let Some(prev) = duplicate.insert(AppPath::from(page_path.clone()), page_path.clone()) {
if prev != page_path {
DuplicateParallelRouteIssue {
app_dir,
page: loader_tree.page.clone(),
}
.cell()
.emit();
}
}
}
Ok(())
}
/// creates the loader tree for a specific route (pathname / [AppPath])
#[turbo_tasks::function]
async fn directory_tree_to_loader_tree(
@ -802,6 +883,8 @@ async fn directory_tree_to_loader_tree(
}
}
let mut duplicate = FxHashMap::default();
for (subdir_name, subdirectory) in &directory_tree.subdirectories {
let parallel_route_key = match_parallel_route(subdir_name);
@ -841,6 +924,10 @@ async fn directory_tree_to_loader_tree(
continue;
}
if *subtree.has_page().await? {
check_duplicate(&mut duplicate, subtree, app_dir).await?;
}
if let Some(current_tree) = tree.parallel_routes.get("children") {
if is_current_directory_catchall && *subtree.has_only_catchall().await? {
// there's probably already a more specific page in the

View file

@ -2232,7 +2232,8 @@
"parallel-routes-and-interception route intercepting should render modal when paired with parallel routes",
"parallel-routes-and-interception parallel routes should display all parallel route params with useParams",
"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 an intercepted route from a slot",
"parallel-routes-and-interception route intercepting should render modal when paired with parallel routes"
],
"pending": [],
"flakey": [],
@ -2266,10 +2267,10 @@
"runtimeError": false
},
"test/e2e/app-dir/parallel-routes-catchall-groups/parallel-routes-catchall-groups.test.ts": {
"passed": [
"passed": [],
"failed": [
"parallel-routes-catchall-groups should work without throwing any errors about conflicting paths"
],
"failed": [],
"pending": [],
"flakey": [],
"runtimeError": false

View file

@ -3341,10 +3341,10 @@
"runtimeError": false
},
"test/e2e/app-dir/conflicting-page-segments/conflicting-page-segments.test.ts": {
"passed": [],
"failed": [
"passed": [
"conflicting-page-segments should throw an error when a route groups causes a conflict with a parallel segment"
],
"failed": [],
"pending": [],
"flakey": [],
"runtimeError": false