diff --git a/packages/next-swc/crates/napi/src/app_structure.rs b/packages/next-swc/crates/napi/src/app_structure.rs index d54fa06b30d0..659f02288dd8 100644 --- a/packages/next-swc/crates/napi/src/app_structure.rs +++ b/packages/next-swc/crates/napi/src/app_structure.rs @@ -41,7 +41,8 @@ async fn project_fs(project_dir: &str, watching: bool) -> Result { struct LoaderTreeForJs { segment: String, parallel_routes: HashMap, - components: serde_json::Value, + #[turbo_tasks(trace_ignore)] + components: ComponentsForJs, } #[derive(PartialEq, Eq, Serialize, Deserialize, ValueDebugFormat, TraceRawVcs)] @@ -69,10 +70,57 @@ async fn fs_path_to_path(project_path: FileSystemPathVc, path: FileSystemPathVc) } } +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, ValueDebugFormat)] +#[serde(rename_all = "camelCase")] +struct ComponentsForJs { + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + layout: Option, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, + #[serde(skip_serializing_if = "Option::is_none")] + loading: Option, + #[serde(skip_serializing_if = "Option::is_none")] + template: Option, + #[serde(skip_serializing_if = "Option::is_none")] + default: Option, + #[serde(skip_serializing_if = "Option::is_none")] + route: Option, + metadata: MetadataForJs, +} + +#[derive(Default, Deserialize, Serialize, PartialEq, Eq, ValueDebugFormat)] +#[serde(rename_all = "camelCase")] +struct MetadataForJs { + #[serde(skip_serializing_if = "Vec::is_empty")] + icon: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + apple: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + twitter: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + open_graph: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + favicon: Vec, +} + +#[derive(Deserialize, Serialize, PartialEq, Eq, ValueDebugFormat)] +#[serde(tag = "type", rename_all = "camelCase")] +enum MetadataForJsItem { + Static { + path: String, + alt_path: Option, + }, + Dynamic { + path: String, + }, +} + async fn prepare_components_for_js( project_path: FileSystemPathVc, components: ComponentsVc, -) -> Result { +) -> Result { let Components { page, layout, @@ -83,80 +131,66 @@ async fn prepare_components_for_js( route, metadata, } = &*components.await?; - let mut map = serde_json::value::Map::new(); + let mut result = ComponentsForJs::default(); async fn add( - map: &mut serde_json::value::Map, + result: &mut Option, project_path: FileSystemPathVc, - key: &str, value: &Option, ) -> Result<()> { if let Some(value) = value { - map.insert( - key.to_string(), - fs_path_to_path(project_path, *value).await?.into(), - ); + *result = Some(fs_path_to_path(project_path, *value).await?); } Ok::<_, anyhow::Error>(()) } - add(&mut map, project_path, "page", page).await?; - add(&mut map, project_path, "layout", layout).await?; - add(&mut map, project_path, "error", error).await?; - add(&mut map, project_path, "loading", loading).await?; - add(&mut map, project_path, "template", template).await?; - add(&mut map, project_path, "default", default).await?; - add(&mut map, project_path, "route", route).await?; - let mut meta = serde_json::value::Map::new(); + add(&mut result.page, project_path, page).await?; + add(&mut result.layout, project_path, layout).await?; + add(&mut result.error, project_path, error).await?; + add(&mut result.loading, project_path, loading).await?; + add(&mut result.template, project_path, template).await?; + add(&mut result.default, project_path, default).await?; + add(&mut result.route, project_path, route).await?; async fn add_meta<'a>( - meta: &mut serde_json::value::Map, + meta: &mut Vec, project_path: FileSystemPathVc, - key: &str, value: impl Iterator, ) -> Result<()> { let mut value = value.peekable(); if value.peek().is_some() { - meta.insert( - key.to_string(), - value - .map(|value| async move { - let mut map = serde_json::value::Map::new(); - match value { - MetadataWithAltItem::Static { path, alt_path } => { - map.insert("type".to_string(), "static".into()); - let path = fs_path_to_path(project_path, *path).await?; - map.insert("path".to_string(), path.into()); - if let Some(alt_path) = alt_path { - let alt_path = fs_path_to_path(project_path, *alt_path).await?; - map.insert("altPath".to_string(), alt_path.into()); - } - } - MetadataWithAltItem::Dynamic { path } => { - map.insert("type".to_string(), "dynamic".into()); - let path = fs_path_to_path(project_path, *path).await?; - map.insert("path".to_string(), path.into()); - } + *meta = value + .map(|value| async move { + Ok(match value { + MetadataWithAltItem::Static { path, alt_path } => { + let path = fs_path_to_path(project_path, *path).await?; + let alt_path = if let Some(alt_path) = alt_path { + Some(fs_path_to_path(project_path, *alt_path).await?) + } else { + None + }; + MetadataForJsItem::Static { path, alt_path } + } + MetadataWithAltItem::Dynamic { path } => { + let path = fs_path_to_path(project_path, *path).await?; + MetadataForJsItem::Dynamic { path } } - Ok(serde_json::Value::from(map)) }) - .try_join() - .await? - .into(), - ); + }) + .try_join() + .await?; } Ok::<_, anyhow::Error>(()) } - add_meta(&mut meta, project_path, "icon", metadata.icon.iter()).await?; - add_meta(&mut meta, project_path, "apple", metadata.apple.iter()).await?; - add_meta(&mut meta, project_path, "twitter", metadata.twitter.iter()).await?; + let meta = &mut result.metadata; + add_meta(&mut meta.icon, project_path, metadata.icon.iter()).await?; + add_meta(&mut meta.apple, project_path, metadata.apple.iter()).await?; + add_meta(&mut meta.twitter, project_path, metadata.twitter.iter()).await?; add_meta( - &mut meta, + &mut meta.open_graph, project_path, - "openGraph", metadata.open_graph.iter(), ) .await?; - add_meta(&mut meta, project_path, "favicon", metadata.favicon.iter()).await?; - map.insert("metadata".to_string(), meta.into()); - Ok(map.into()) + add_meta(&mut meta.favicon, project_path, metadata.favicon.iter()).await?; + Ok(result) } #[tasks::function] 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 b029223ff42d..00c85d28c40e 100644 --- a/packages/next-swc/crates/next-core/src/app_source.rs +++ b/packages/next-swc/crates/next-core/src/app_source.rs @@ -470,27 +470,27 @@ async fn create_global_metadata_source( let metadata = metadata.await?; let mut unsupported_metadata = Vec::new(); let mut sources = Vec::new(); - let mut handle = |server_path, item| { - if let Some(item) = item { - match item { - MetadataItem::Static { path } => { - let asset = FixedStaticAssetVc::new( - server_root.join(server_path), - SourceAssetVc::new(path).into(), - ); - sources.push( - AssetGraphContentSourceVc::new_eager(server_root, asset.into()).into(), - ) - } - MetadataItem::Dynamic { path } => { - unsupported_metadata.push(path); - } + for (server_path, item) in [ + ("robots.txt", metadata.robots), + ("favicon.ico", metadata.favicon), + ("sitemap.xml", metadata.sitemap), + ] { + let Some(item) = item else { + continue; + }; + match item { + MetadataItem::Static { path } => { + let asset = FixedStaticAssetVc::new( + server_root.join(server_path), + SourceAssetVc::new(path).into(), + ); + sources.push(AssetGraphContentSourceVc::new_eager(server_root, asset.into()).into()) + } + MetadataItem::Dynamic { path } => { + unsupported_metadata.push(path); } } - }; - handle("robots.txt", metadata.robots); - handle("favicon.ico", metadata.favicon); - handle("sitemap.xml", metadata.sitemap); + } if !unsupported_metadata.is_empty() { UnsupportedDynamicMetadataIssue { app_dir, @@ -630,6 +630,14 @@ impl AppRendererVc { unsupported_metadata: Vec, } + impl State { + fn unique_number(&mut self) -> usize { + let i = self.counter; + self.counter += 1; + i + } + } + let mut state = State { inner_assets: IndexMap::new(), counter: 0, @@ -647,8 +655,7 @@ impl AppRendererVc { use std::fmt::Write; if let Some(component) = component { - let i = state.counter; - state.counter += 1; + let i = state.unique_number(); let identifier = magic_identifier::mangle(&format!("{name} #{i}")); let chunks_identifier = magic_identifier::mangle(&format!("chunks of {name} #{i}")); writeln!( @@ -708,8 +715,7 @@ import {}, {{ chunks as {} }} from "COMPONENT_{}"; match manifest { MetadataItem::Static { path } => { use std::fmt::Write; - let i = state.counter; - state.counter += 1; + let i = state.unique_number(); let identifier = magic_identifier::mangle(&format!("manifest #{i}")); let inner_module_id = format!("METADATA_{i}"); state @@ -754,8 +760,7 @@ import {}, {{ chunks as {} }} from "COMPONENT_{}"; item: &MetadataWithAltItem, ) -> Result<()> { use std::fmt::Write; - let i = state.counter; - state.counter += 1; + let i = state.unique_number(); let identifier = magic_identifier::mangle(&format!("{name} #{i}")); let inner_module_id = format!("METADATA_{i}"); state 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 ca05067377eb..eb7482f2645f 100644 --- a/packages/next-swc/crates/next-core/src/app_structure.rs +++ b/packages/next-swc/crates/next-core/src/app_structure.rs @@ -82,6 +82,7 @@ impl ComponentsVc { } } +/// A single metadata file plus an optional "alt" text file. #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, TraceRawVcs)] pub enum MetadataWithAltItem { Static { @@ -93,12 +94,14 @@ pub enum MetadataWithAltItem { }, } +/// A single metadata file. #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, TraceRawVcs)] pub enum MetadataItem { Static { path: FileSystemPathVc }, Dynamic { path: FileSystemPathVc }, } +/// Metadata file that can be placed in any segment of the app directory. #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, TraceRawVcs)] pub struct Metadata { #[serde(skip_serializing_if = "Vec::is_empty")] @@ -150,6 +153,7 @@ impl Metadata { } } +/// Metadata files that can be placed in the root of the app directory. #[turbo_tasks::value] #[derive(Default, Clone, Debug)] pub struct GlobalMetadata { @@ -277,7 +281,7 @@ fn match_metadata_file<'a>( ) -> Option<(&'a str, bool)> { let (stem, ext) = basename.split_once('.')?; static REGEX: Lazy = Lazy::new(|| Regex::new("^(.*?)\\d*$").unwrap()); - let captures = REGEX.captures(stem).unwrap(); + let captures = REGEX.captures(stem).expect("the regex will always match"); let stem = captures.get(1).unwrap().as_str(); if page_extensions.iter().any(|e| e == ext) { return Some((stem, true)); @@ -366,7 +370,7 @@ async fn get_directory_tree( let result = get_directory_tree(dir, page_extensions); subdirectories.insert(basename.to_string(), result); } - // TODO handle symlinks in app dir + // TODO(WEB-952) handle symlinks in app dir _ => {} } } @@ -668,6 +672,7 @@ async fn directory_tree_to_entrypoints_internal( Ok(EntrypointsVc::cell(result)) } +/// Returns the global metadata for an app directory. #[turbo_tasks::function] pub async fn get_global_metadata( app_dir: FileSystemPathVc, diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/opengraph-image.alt.txt b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/opengraph-image.alt.txt index 9b7811b23ab6..e07d9e8a6703 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/opengraph-image.alt.txt +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/opengraph-image.alt.txt @@ -1 +1 @@ -This is an alt text. +This is an alt text. \ No newline at end of file