diff --git a/packages/next-swc/crates/next-api/src/app.rs b/packages/next-swc/crates/next-api/src/app.rs index 12ed8e549887..295b57369a6d 100644 --- a/packages/next-swc/crates/next-api/src/app.rs +++ b/packages/next-swc/crates/next-api/src/app.rs @@ -8,7 +8,7 @@ use next_core::{ mode::NextMode, next_app::{ get_app_client_references_chunks, get_app_client_shared_chunks, get_app_page_entry, - get_app_route_entry, AppEntry, + get_app_route_entry, AppEntry, AppPage, }, next_client::{ get_client_module_options_context, get_client_resolve_options_context, @@ -344,8 +344,7 @@ impl AppProject { .map(|(pathname, app_entrypoint)| async { Ok(( pathname.clone(), - *app_entry_point_to_route(self, app_entrypoint.clone(), pathname.clone()) - .await?, + *app_entry_point_to_route(self, app_entrypoint.clone()).await?, )) }) .try_join() @@ -360,13 +359,9 @@ impl AppProject { pub async fn app_entry_point_to_route( app_project: Vc, entrypoint: AppEntrypoint, - pathname: String, ) -> Vc { match entrypoint { - AppEntrypoint::AppPage { - original_name, - loader_tree, - } => Route::AppPage { + AppEntrypoint::AppPage { page, loader_tree } => Route::AppPage { html_endpoint: Vc::upcast( AppEndpoint { ty: AppEndpointType::Page { @@ -374,8 +369,7 @@ pub async fn app_entry_point_to_route( loader_tree, }, app_project, - pathname: pathname.clone(), - original_name: original_name.clone(), + page: page.clone(), } .cell(), ), @@ -386,22 +380,17 @@ pub async fn app_entry_point_to_route( loader_tree, }, app_project, - pathname, - original_name, + page, } .cell(), ), }, - AppEntrypoint::AppRoute { - original_name, - path, - } => Route::AppRoute { + AppEntrypoint::AppRoute { page, path } => Route::AppRoute { endpoint: Vc::upcast( AppEndpoint { ty: AppEndpointType::Route { path }, app_project, - pathname, - original_name, + page, } .cell(), ), @@ -431,8 +420,7 @@ enum AppEndpointType { struct AppEndpoint { ty: AppEndpointType, app_project: Vc, - pathname: String, - original_name: String, + page: AppPage, } #[turbo_tasks::value_impl] @@ -444,8 +432,7 @@ impl AppEndpoint { self.app_project.edge_rsc_module_context(), loader_tree, self.app_project.app_dir(), - self.pathname.clone(), - self.original_name.clone(), + self.page.clone(), self.app_project.project().project_path(), ) } @@ -456,8 +443,7 @@ impl AppEndpoint { self.app_project.rsc_module_context(), self.app_project.edge_rsc_module_context(), Vc::upcast(FileSource::new(path)), - self.pathname.clone(), - self.original_name.clone(), + self.page.clone(), self.app_project.project().project_path(), ) } diff --git a/packages/next-swc/crates/next-build/src/next_app/app_entries.rs b/packages/next-swc/crates/next-build/src/next_app/app_entries.rs index 973b30ea7d54..9c7271ecdd66 100644 --- a/packages/next-swc/crates/next-build/src/next_app/app_entries.rs +++ b/packages/next-swc/crates/next-build/src/next_app/app_entries.rs @@ -187,31 +187,23 @@ pub async fn get_app_entries( let mut entries = entrypoints .await? .iter() - .map(|(pathname, entrypoint)| async move { + .map(|(_, entrypoint)| async move { Ok(match entrypoint { - Entrypoint::AppPage { - original_name, - loader_tree, - } => get_app_page_entry( + Entrypoint::AppPage { page, loader_tree } => get_app_page_entry( rsc_context, // TODO add edge support rsc_context, *loader_tree, app_dir, - pathname.clone(), - original_name.clone(), + page.clone(), project_root, ), - Entrypoint::AppRoute { - original_name, - path, - } => get_app_route_entry( + Entrypoint::AppRoute { page, path } => get_app_route_entry( rsc_context, // TODO add edge support rsc_context, Vc::upcast(FileSource::new(*path)), - pathname.clone(), - original_name.clone(), + page.clone(), project_root, ), }) diff --git a/packages/next-swc/crates/next-core/Cargo.toml b/packages/next-swc/crates/next-core/Cargo.toml index 5b50aff1185c..38bcb02a3cda 100644 --- a/packages/next-swc/crates/next-core/Cargo.toml +++ b/packages/next-swc/crates/next-core/Cargo.toml @@ -39,6 +39,7 @@ turbopack-binding = { workspace = true, features = [ "__turbo_tasks_hash", "__turbopack", "__turbopack_build", + "__turbopack_cli_utils", "__turbopack_core", "__turbopack_dev", "__turbopack_dev_server", diff --git a/packages/next-swc/crates/next-core/src/app_source.rs b/packages/next-swc/crates/next-core/src/app_source.rs index e5af263c3d6b..cf35f3795f88 100644 --- a/packages/next-swc/crates/next-core/src/app_source.rs +++ b/packages/next-swc/crates/next-core/src/app_source.rs @@ -65,7 +65,7 @@ use crate::{ fallback::get_fallback_page, loader_tree::{LoaderTreeModule, ServerComponentTransition}, mode::NextMode, - next_app::UnsupportedDynamicMetadataIssue, + next_app::{AppPage, AppPath, PathSegment, UnsupportedDynamicMetadataIssue}, next_client::{ context::{ get_client_assets_path, get_client_module_options_context, @@ -95,31 +95,28 @@ use crate::{ util::{render_data, NextRuntime}, }; -fn pathname_to_segments(pathname: &str) -> Result<(Vec, RouteType)> { +fn app_path_to_segments(path: &AppPath) -> Result<(Vec, RouteType)> { let mut segments = Vec::new(); - let mut split = pathname.split('/'); - while let Some(segment) = split.next() { - if segment.is_empty() - || (segment.starts_with('(') && segment.ends_with(')') || segment.starts_with('@')) - { - // ignore - } else if segment.starts_with("[[...") && segment.ends_with("]]") - || segment.starts_with("[...") && segment.ends_with(']') - { - // (optional) catch all segment - if split.remainder().is_some() { - bail!( - "Invalid route {}, catch all segment must be the last segment", - pathname - ) + let mut iter = path.iter().peekable(); + + while let Some(segment) = iter.next() { + match segment { + PathSegment::Static(s) => { + segments.push(BaseSegment::Static(s.to_string())); + } + PathSegment::Dynamic(_) => { + segments.push(BaseSegment::Dynamic); + } + PathSegment::CatchAll(_) | PathSegment::OptionalCatchAll(_) => { + if iter.peek().is_some() { + bail!( + "Invalid route {}, catch all segment must be the last segment", + path + ) + } + + return Ok((segments, RouteType::CatchAll)); } - return Ok((segments, RouteType::CatchAll)); - } else if segment.starts_with('[') || segment.ends_with(']') { - // dynamic segment - segments.push(BaseSegment::Dynamic); - } else { - // normal segment - segments.push(BaseSegment::Static(segment.to_string())); } } Ok((segments, RouteType::Exact)) @@ -654,12 +651,12 @@ pub async fn create_app_source( let entrypoints = entrypoints.await?; let mut sources: Vec<_> = entrypoints .iter() - .map(|(pathname, entrypoint)| match *entrypoint { + .map(|(_, entrypoint)| match *entrypoint { Entrypoint::AppPage { - original_name: _, + ref page, loader_tree, } => create_app_page_source_for_route( - pathname.clone(), + page.clone(), loader_tree, context_ssr, context, @@ -672,11 +669,8 @@ pub async fn create_app_source( output_path, render_data, ), - Entrypoint::AppRoute { - original_name: _, - path, - } => create_app_route_source_for_route( - pathname.clone(), + Entrypoint::AppRoute { ref page, path } => create_app_route_source_for_route( + page.clone(), path, context_ssr, project_path, @@ -696,7 +690,7 @@ pub async fn create_app_source( .collect(); if let Some(&Entrypoint::AppPage { - original_name: _, + page: _, loader_tree, }) = entrypoints.get("/_not-found") { @@ -769,7 +763,7 @@ async fn create_global_metadata_source( #[turbo_tasks::function] async fn create_app_page_source_for_route( - pathname: String, + page: AppPage, loader_tree: Vc, context_ssr: Vc, context: Vc, @@ -782,11 +776,12 @@ async fn create_app_page_source_for_route( intermediate_output_path_root: Vc, render_data: Vc, ) -> Result>> { - let pathname_vc = Vc::cell(pathname.clone()); + let app_path = AppPath::from(page.clone()); + let pathname_vc = Vc::cell(app_path.to_string()); let params_matcher = NextParamsMatcher::new(pathname_vc); - let (base_segments, route_type) = pathname_to_segments(&pathname)?; + let (base_segments, route_type) = app_path_to_segments(&app_path)?; let source = create_node_rendered_source( project_path, @@ -814,7 +809,7 @@ async fn create_app_page_source_for_route( should_debug("app_source"), ); - Ok(source.issue_file_path(app_dir, format!("Next.js App Page Route {pathname}"))) + Ok(source.issue_file_path(app_dir, format!("Next.js App Page Route {app_path}"))) } #[turbo_tasks::function] @@ -864,7 +859,7 @@ async fn create_app_not_found_page_source( #[turbo_tasks::function] async fn create_app_route_source_for_route( - pathname: String, + page: AppPage, entry_path: Vc, context_ssr: Vc, project_path: Vc, @@ -875,11 +870,12 @@ async fn create_app_route_source_for_route( intermediate_output_path_root: Vc, render_data: Vc, ) -> Result>> { - let pathname_vc = Vc::cell(pathname.to_string()); + let app_path = AppPath::from(page.clone()); + let pathname_vc = Vc::cell(app_path.to_string()); let params_matcher = NextParamsMatcher::new(pathname_vc); - let (base_segments, route_type) = pathname_to_segments(&pathname)?; + let (base_segments, route_type) = app_path_to_segments(&app_path)?; let source = create_node_api_source( project_path, @@ -906,7 +902,7 @@ async fn create_app_route_source_for_route( should_debug("app_source"), ); - Ok(source.issue_file_path(app_dir, format!("Next.js App Route {pathname}"))) + Ok(source.issue_file_path(app_dir, format!("Next.js App Route {app_path}"))) } /// The renderer for pages in app directory 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 c09ccdc644c9..a3597079af6c 100644 --- a/packages/next-swc/crates/next-core/src/app_structure.rs +++ b/packages/next-swc/crates/next-core/src/app_structure.rs @@ -1,7 +1,11 @@ use std::collections::{BTreeMap, HashMap}; use anyhow::{bail, Result}; -use indexmap::{indexmap, map::Entry, IndexMap}; +use indexmap::{ + indexmap, + map::{Entry, OccupiedEntry}, + IndexMap, +}; use once_cell::sync::Lazy; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -14,7 +18,11 @@ use turbopack_binding::{ turbopack::core::issue::{Issue, IssueExt, IssueSeverity}, }; -use crate::{next_config::NextConfig, next_import_map::get_next_package}; +use crate::{ + next_app::{AppPage, AppPath}, + next_config::NextConfig, + next_import_map::get_next_package, +}; /// A final route in the app directory. #[turbo_tasks::value] @@ -447,11 +455,11 @@ async fn merge_loader_trees( )] pub enum Entrypoint { AppPage { - original_name: String, + page: AppPage, loader_tree: Vc, }, AppRoute { - original_name: String, + page: AppPage, path: Vc, }, } @@ -487,131 +495,119 @@ async fn add_parallel_route( Ok(()) } +fn conflict_issue( + app_dir: Vc, + e: &OccupiedEntry, + a: &str, + b: &str, + value_a: &AppPage, + value_b: &AppPage, +) { + let item_names = if a == b { + format!("{}s", a) + } else { + format!("{} and {}", a, b) + }; + + DirectoryTreeIssue { + app_dir, + message: Vc::cell(format!( + "Conflicting {} at {}: {a} at {value_a} and {b} at {value_b}", + item_names, + e.key(), + )), + severity: IssueSeverity::Error.cell(), + } + .cell() + .emit(); +} + async fn add_app_page( app_dir: Vc, result: &mut IndexMap, - key: String, - original_name: String, + page: AppPage, loader_tree: Vc, ) -> Result<()> { - match result.entry(key) { - Entry::Occupied(mut e) => { - let value = e.get(); - match value { - Entrypoint::AppPage { - original_name: existing_original_name, - .. - } => { - if *existing_original_name != original_name { - DirectoryTreeIssue { - app_dir, - message: Vc::cell(format!( - "Conflicting pages at {}: {existing_original_name} and \ - {original_name}", - e.key() - )), - severity: IssueSeverity::Error.cell(), - } - .cell() - .emit(); - return Ok(()); - } - if let Entrypoint::AppPage { - loader_tree: value, .. - } = e.get_mut() - { - *value = merge_loader_trees(app_dir, *value, loader_tree) - .resolve() - .await?; - } - } - Entrypoint::AppRoute { - original_name: existing_original_name, - .. - } => { - DirectoryTreeIssue { - app_dir, - message: Vc::cell(format!( - "Conflicting page and route at {}: route at {existing_original_name} \ - and page at {original_name}", - e.key() - )), - severity: IssueSeverity::Error.cell(), - } - .cell() - .emit(); - return Ok(()); - } + let pathname = AppPath::from(page.clone()); + + let mut e = match result.entry(format!("{pathname}")) { + Entry::Occupied(e) => e, + Entry::Vacant(e) => { + e.insert(Entrypoint::AppPage { page, loader_tree }); + return Ok(()); + } + }; + + let conflict = |existing_name: &str, existing_page: &AppPage| { + conflict_issue(app_dir, &e, "page", existing_name, &page, existing_page); + }; + + let value = e.get(); + match value { + Entrypoint::AppPage { + page: existing_page, + .. + } => { + if *existing_page != page { + conflict("page", existing_page); + return Ok(()); + } + + if let Entrypoint::AppPage { + loader_tree: value, .. + } = e.get_mut() + { + *value = merge_loader_trees(app_dir, *value, loader_tree) + .resolve() + .await?; } } - Entry::Vacant(e) => { - e.insert(Entrypoint::AppPage { - original_name, - loader_tree, - }); + Entrypoint::AppRoute { + page: existing_page, + .. + } => { + conflict("route", existing_page); } } + Ok(()) } -async fn add_app_route( +fn add_app_route( app_dir: Vc, result: &mut IndexMap, - key: String, - original_name: String, + page: AppPage, path: Vc, -) -> Result<()> { - match result.entry(key) { - Entry::Occupied(mut e) => { - let value = e.get(); - match value { - Entrypoint::AppPage { - original_name: existing_original_name, - .. - } => { - DirectoryTreeIssue { - app_dir, - message: Vc::cell(format!( - "Conflicting route and page at {}: route at {original_name} and page \ - at {existing_original_name}", - e.key() - )), - severity: IssueSeverity::Error.cell(), - } - .cell() - .emit(); - } - Entrypoint::AppRoute { - original_name: existing_original_name, - .. - } => { - DirectoryTreeIssue { - app_dir, - message: Vc::cell(format!( - "Conflicting routes at {}: {existing_original_name} and \ - {original_name}", - e.key() - )), - severity: IssueSeverity::Error.cell(), - } - .cell() - .emit(); - return Ok(()); - } - } - *e.get_mut() = Entrypoint::AppRoute { - original_name, - path, - }; - } +) { + let pathname = AppPath::from(page.clone()); + + let e = match result.entry(format!("{pathname}")) { + Entry::Occupied(e) => e, Entry::Vacant(e) => { - e.insert(Entrypoint::AppRoute { - original_name, - path, - }); + e.insert(Entrypoint::AppRoute { page, path }); + return; + } + }; + + let conflict = |existing_name: &str, existing_page: &AppPage| { + conflict_issue(app_dir, &e, "route", existing_name, &page, existing_page); + }; + + let value = e.get(); + match value { + Entrypoint::AppPage { + page: existing_page, + .. + } => { + conflict("page", existing_page); + } + Entrypoint::AppRoute { + page: existing_page, + .. + } => { + conflict("route", existing_page); } } - Ok(()) } #[turbo_tasks::function] @@ -627,13 +623,7 @@ fn directory_tree_to_entrypoints( app_dir: Vc, directory_tree: Vc, ) -> Vc { - directory_tree_to_entrypoints_internal( - app_dir, - "".to_string(), - directory_tree, - "/".to_string(), - "/".to_string(), - ) + directory_tree_to_entrypoints_internal(app_dir, "".to_string(), directory_tree, AppPage::new()) } #[turbo_tasks::function] @@ -641,8 +631,7 @@ async fn directory_tree_to_entrypoints_internal( app_dir: Vc, directory_name: String, directory_tree: Vc, - path_prefix: String, - original_name_prefix: String, + app_page: AppPage, ) -> Result> { let mut result = IndexMap::new(); @@ -657,8 +646,7 @@ async fn directory_tree_to_entrypoints_internal( add_app_page( app_dir, &mut result, - path_prefix.to_string(), - original_name_prefix.to_string(), + app_page.clone(), if current_level_is_parallel_route { LoaderTree { segment: "__PAGE__".to_string(), @@ -697,8 +685,7 @@ async fn directory_tree_to_entrypoints_internal( add_app_page( app_dir, &mut result, - path_prefix.to_string(), - original_name_prefix.to_string(), + app_page.clone(), if current_level_is_parallel_route { LoaderTree { segment: "__DEFAULT__".to_string(), @@ -734,17 +721,11 @@ async fn directory_tree_to_entrypoints_internal( } if let Some(route) = components.route { - add_app_route( - app_dir, - &mut result, - path_prefix.to_string(), - original_name_prefix.to_string(), - route, - ) - .await?; + add_app_route(app_dir, &mut result, app_page.clone(), route); } - if path_prefix == "/" { + // root path: / + if app_page.len() == 0 { // Next.js has this logic in "collect-app-paths", where the root not-found page // is considered as its own entry point. if let Some(_not_found) = components.not_found { @@ -766,22 +747,14 @@ async fn directory_tree_to_entrypoints_internal( } .cell(); - add_app_page( - app_dir, - &mut result, - "/not-found".to_string(), - "/not-found".to_string(), - dev_not_found_tree, - ) - .await?; - add_app_page( - app_dir, - &mut result, - "/_not-found".to_string(), - "/_not-found".to_string(), - dev_not_found_tree, - ) - .await?; + { + let app_page = app_page.clone_push_str("not-found")?; + add_app_page(app_dir, &mut result, app_page, dev_not_found_tree).await?; + } + { + let app_page = app_page.clone_push_str("_not-found")?; + add_app_page(app_dir, &mut result, app_page, dev_not_found_tree).await?; + } } else { // Create default not-found page for production if there's no customized // not-found @@ -803,55 +776,35 @@ async fn directory_tree_to_entrypoints_internal( } .cell(); - add_app_page( - app_dir, - &mut result, - "/_not-found".to_string(), - "/_not-found".to_string(), - prod_not_found_tree, - ) - .await?; + let app_page = app_page.clone_push_str("_not-found")?; + add_app_page(app_dir, &mut result, app_page, prod_not_found_tree).await?; } } for (subdir_name, &subdirectory) in subdirectories.iter() { - let is_route_group = subdir_name.starts_with('(') && subdir_name.ends_with(')'); let parallel_route_key = match_parallel_route(subdir_name); + + let mut app_page = app_page.clone(); + if parallel_route_key.is_none() { + app_page.push_str(subdir_name)?; + } + let map = directory_tree_to_entrypoints_internal( app_dir, subdir_name.to_string(), subdirectory, - if is_route_group || parallel_route_key.is_some() { - path_prefix.clone() - } else if path_prefix == "/" { - format!("/{subdir_name}") - } else { - format!("{path_prefix}/{subdir_name}") - }, - if parallel_route_key.is_some() { - original_name_prefix.clone() - } else if original_name_prefix == "/" { - format!("/{subdir_name}") - } else { - format!("{original_name_prefix}/{subdir_name}") - }, + app_page, ) .await?; - for (full_path, entrypoint) in map.iter() { + + for (_, entrypoint) in map.iter() { match *entrypoint { Entrypoint::AppPage { - ref original_name, + ref page, loader_tree, } => { if current_level_is_parallel_route { - add_app_page( - app_dir, - &mut result, - full_path.clone(), - original_name.clone(), - loader_tree, - ) - .await?; + 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 { @@ -862,28 +815,11 @@ async fn directory_tree_to_entrypoints_internal( components: components.without_leafs().cell(), } .cell(); - add_app_page( - app_dir, - &mut result, - full_path.clone(), - original_name.clone(), - child_loader_tree, - ) - .await?; + add_app_page(app_dir, &mut result, page.clone(), child_loader_tree).await?; } } - Entrypoint::AppRoute { - ref original_name, - path, - } => { - add_app_route( - app_dir, - &mut result, - full_path.clone(), - original_name.clone(), - path, - ) - .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/app_favicon_entry.rs b/packages/next-swc/crates/next-core/src/next_app/app_favicon_entry.rs index 0f4df5eb01f3..b72fc9af399a 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_favicon_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_favicon_entry.rs @@ -14,7 +14,10 @@ use turbopack_binding::{ }; use super::app_route_entry::get_app_route_entry; -use crate::{app_structure::MetadataItem, next_app::AppEntry}; +use crate::{ + app_structure::MetadataItem, + next_app::{AppEntry, AppPage, PageSegment}, +}; /// Computes the entry for a Next.js favicon file. #[turbo_tasks::function] @@ -57,7 +60,7 @@ pub async fn get_app_route_favicon_entry( const contentType = {content_type} const cacheControl = {cache_control} const buffer = Buffer.from({original_file_content_b64}, 'base64') - + export function GET() {{ return new NextResponse(buffer, {{ headers: {{ @@ -66,7 +69,7 @@ pub async fn get_app_route_favicon_entry( }}, }}) }} - + export const dynamic = 'force-static' "#, content_type = StringifyJs(&content_type), @@ -84,8 +87,7 @@ pub async fn get_app_route_favicon_entry( edge_context, Vc::upcast(source), // TODO(alexkirsz) Get this from the metadata? - "/favicon.ico".to_string(), - "/favicon.ico".to_string(), + AppPage(vec![PageSegment::Static("/favicon.ico".to_string())]), project_root, )) } diff --git a/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs b/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs index bde79e2c38b4..fd99d2566467 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs @@ -19,7 +19,7 @@ use crate::{ app_structure::LoaderTree, loader_tree::{LoaderTreeModule, ServerComponentTransition}, mode::NextMode, - next_app::UnsupportedDynamicMetadataIssue, + next_app::{AppPage, AppPath, UnsupportedDynamicMetadataIssue}, next_server_component::NextServerComponentTransition, parse_segment_config_from_loader_tree, util::{load_next_js_template, virtual_next_js_template_path, NextRuntime}, @@ -32,8 +32,7 @@ pub async fn get_app_page_entry( edge_context: Vc, loader_tree: Vc, app_dir: Vc, - pathname: String, - original_name: String, + page: AppPage, project_root: Vc, ) -> Result> { let config = parse_segment_config_from_loader_tree(loader_tree, Vc::upcast(nodejs_context)); @@ -79,6 +78,9 @@ pub async fn get_app_page_entry( let pages = pages.iter().map(|page| page.to_string()).try_join().await?; + let original_name = page.to_string(); + let pathname = AppPath::from(page.clone()).to_string(); + let original_page_name = get_original_page_name(&original_name); let template_file = "build/templates/app-page.js"; @@ -90,7 +92,7 @@ pub async fn get_app_page_entry( .to_str()? .replace( "\"VAR_DEFINITION_PAGE\"", - &StringifyJs(&original_name).to_string(), + &StringifyJs(&page.to_string()).to_string(), ) .replace( "\"VAR_DEFINITION_PATHNAME\"", diff --git a/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs b/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs index 45cf5470f40e..e60c963e0ece 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs @@ -21,7 +21,7 @@ use turbopack_binding::{ }; use crate::{ - next_app::AppEntry, + next_app::{AppEntry, AppPage, AppPath}, parse_segment_config_from_source, util::{load_next_js_template, virtual_next_js_template_path, NextRuntime}, }; @@ -32,8 +32,7 @@ pub async fn get_app_route_entry( nodejs_context: Vc, edge_context: Vc, source: Vc>, - pathname: String, - original_name: String, + page: AppPage, project_root: Vc, ) -> Result> { let config = parse_segment_config_from_source( @@ -52,6 +51,9 @@ pub async fn get_app_route_entry( let mut result = RopeBuilder::default(); + let original_name = page.to_string(); + let pathname = AppPath::from(page.clone()).to_string(); + let original_page_name = get_original_route_name(&original_name); let path = source.ident().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 7f40ca34fb94..885ecdf4cd71 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 @@ -6,12 +6,283 @@ pub(crate) mod app_page_entry; pub(crate) mod app_route_entry; pub(crate) mod unsupported_dynamic_metadata_issue; -pub use app_client_references_chunks::{ - get_app_client_references_chunks, ClientReferenceChunks, ClientReferencesChunks, +use std::{ + fmt::{Display, Formatter, Write}, + ops::Deref, }; -pub use app_client_shared_chunks::get_app_client_shared_chunks; -pub use app_entry::AppEntry; -pub use app_favicon_entry::get_app_route_favicon_entry; -pub use app_page_entry::get_app_page_entry; -pub use app_route_entry::get_app_route_entry; -pub use unsupported_dynamic_metadata_issue::UnsupportedDynamicMetadataIssue; + +use anyhow::{bail, Result}; +use serde::{Deserialize, Serialize}; +use turbo_tasks::{trace::TraceRawVcs, TaskInput}; + +pub use crate::next_app::{ + app_client_references_chunks::{ + get_app_client_references_chunks, ClientReferenceChunks, ClientReferencesChunks, + }, + app_client_shared_chunks::get_app_client_shared_chunks, + app_entry::AppEntry, + app_favicon_entry::get_app_route_favicon_entry, + app_page_entry::get_app_page_entry, + app_route_entry::get_app_route_entry, + unsupported_dynamic_metadata_issue::UnsupportedDynamicMetadataIssue, +}; + +#[derive(Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq, TaskInput, TraceRawVcs)] +pub enum PageSegment { + Static(String), + Dynamic(String), + CatchAll(String), + OptionalCatchAll(String), + Group(String), + Parallel(String), + PageType(PageType), +} + +impl PageSegment { + pub fn parse(segment: &str) -> Result { + if segment.is_empty() { + bail!("empty segments are not allowed"); + } + + if segment.contains('/') { + bail!("slashes are not allowed in segments"); + } + + if let Some(s) = segment.strip_prefix('(').and_then(|s| s.strip_suffix(')')) { + return Ok(PageSegment::Group(s.to_string())); + } + + if let Some(s) = segment.strip_prefix('@') { + return Ok(PageSegment::Parallel(s.to_string())); + } + + if let Some(s) = segment + .strip_prefix("[[...") + .and_then(|s| s.strip_suffix("]]")) + { + return Ok(PageSegment::OptionalCatchAll(s.to_string())); + } + + if let Some(s) = segment + .strip_prefix("[...") + .and_then(|s| s.strip_suffix(']')) + { + return Ok(PageSegment::CatchAll(s.to_string())); + } + + if let Some(s) = segment.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { + return Ok(PageSegment::Dynamic(s.to_string())); + } + + Ok(PageSegment::Static(segment.to_string())) + } +} + +impl Display for PageSegment { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PageSegment::Static(s) => f.write_str(s), + PageSegment::Dynamic(s) => { + f.write_char('[')?; + f.write_str(s)?; + f.write_char(']') + } + PageSegment::CatchAll(s) => { + f.write_str("[...")?; + f.write_str(s)?; + f.write_char(']') + } + PageSegment::OptionalCatchAll(s) => { + f.write_str("[[...")?; + f.write_str(s)?; + f.write_str("]]") + } + PageSegment::Group(s) => { + f.write_char('(')?; + f.write_str(s)?; + f.write_char(')') + } + PageSegment::Parallel(s) => { + f.write_char('@')?; + f.write_str(s) + } + PageSegment::PageType(s) => Display::fmt(s, f), + } + } +} + +#[derive(Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq, TaskInput, TraceRawVcs)] +pub enum PageType { + Page, + Route, +} + +impl Display for PageType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + PageType::Page => "page", + PageType::Route => "route", + }) + } +} + +/// Describes the pathname including all internal modifiers such as +/// intercepting routes, parallel routes and route/page suffixes that are not +/// part of the pathname. +#[derive( + Clone, Debug, Hash, PartialEq, Eq, Default, Serialize, Deserialize, TaskInput, TraceRawVcs, +)] +pub struct AppPage(pub Vec); + +impl AppPage { + pub fn new() -> Self { + Self::default() + } + + pub fn push(&mut self, segment: PageSegment) -> Result<()> { + if matches!( + self.0.last(), + Some(PageSegment::CatchAll(..) | PageSegment::OptionalCatchAll(..)) + ) && !matches!(segment, PageSegment::PageType(..)) + { + bail!( + "Invalid segment {}, catch all segment must be the last segment", + segment + ) + } + + self.0.push(segment); + Ok(()) + } + + pub fn push_str(&mut self, segment: &str) -> Result<()> { + if segment.is_empty() { + return Ok(()); + } + + self.push(PageSegment::parse(segment)?) + } + + pub fn clone_push(&self, segment: PageSegment) -> Result { + let mut cloned = self.clone(); + cloned.push(segment)?; + Ok(cloned) + } + + pub fn clone_push_str(&self, segment: &str) -> Result { + let mut cloned = self.clone(); + cloned.push_str(segment)?; + Ok(cloned) + } + + pub fn parse(page: &str) -> Result { + let mut app_page = Self::new(); + + for segment in page.split('/') { + app_page.push_str(segment)?; + } + + Ok(app_page) + } +} + +impl Display for AppPage { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.0.is_empty() { + return f.write_char('/'); + } + + for segment in &self.0 { + f.write_char('/')?; + Display::fmt(segment, f)?; + } + + Ok(()) + } +} + +impl Deref for AppPage { + type Target = [PageSegment]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq, TaskInput, TraceRawVcs)] +pub enum PathSegment { + Static(String), + Dynamic(String), + CatchAll(String), + OptionalCatchAll(String), +} + +impl Display for PathSegment { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PathSegment::Static(s) => f.write_str(s), + PathSegment::Dynamic(s) => { + f.write_char('[')?; + f.write_str(s)?; + f.write_char(']') + } + PathSegment::CatchAll(s) => { + f.write_str("[...")?; + f.write_str(s)?; + f.write_char(']') + } + PathSegment::OptionalCatchAll(s) => { + f.write_str("[[...")?; + f.write_str(s)?; + f.write_str("]]") + } + } + } +} + +/// The pathname (including dynamic placeholders) for a route to resolve. +#[derive( + Clone, Debug, Hash, PartialEq, Eq, Default, Serialize, Deserialize, TaskInput, TraceRawVcs, +)] +pub struct AppPath(pub Vec); + +impl Deref for AppPath { + type Target = [PathSegment]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for AppPath { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.0.is_empty() { + return f.write_char('/'); + } + + for segment in &self.0 { + f.write_char('/')?; + Display::fmt(segment, f)?; + } + + Ok(()) + } +} + +impl From for AppPath { + fn from(value: AppPage) -> Self { + AppPath( + value + .0 + .into_iter() + .filter_map(|segment| match segment { + PageSegment::Static(s) => Some(PathSegment::Static(s)), + PageSegment::Dynamic(s) => Some(PathSegment::Dynamic(s)), + PageSegment::CatchAll(s) => Some(PathSegment::CatchAll(s)), + PageSegment::OptionalCatchAll(s) => Some(PathSegment::OptionalCatchAll(s)), + _ => None, + }) + .collect(), + ) + } +}