From b50ecb7e53727923ab9a9714b534aca93f68f70d Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 27 Apr 2023 07:54:53 +0200 Subject: [PATCH] add support for static implicit metadata in turbopack (#48823) ### What? * support local static metadata files; opengraph-image, twitter-image, favicon, manifest, icon, apple-icon * support global static metadata files: robots.txt, sitemap.xml, favicon.ico * dynamic metadata is not yet implemented, but yields a warning It's implemented a bit different compared to the webpack version. All images will use the usual image machinery, so they are emitted to output directory, content hashed and the url is shared with the same import import in the app. ### Why? Unsupported, and we want to have that. ### How? see also https://github.com/vercel/turbo/pull/4692 fixes WEB-524 --- .../next-swc/crates/napi/src/app_structure.rs | 139 +++++++---- .../crates/next-core/src/app_source.rs | 225 ++++++++++++++++-- .../crates/next-core/src/app_structure.rs | 198 +++++++++++++-- .../crates/next-core/src/asset_helpers.rs | 27 +++ packages/next-swc/crates/next-core/src/lib.rs | 1 + .../crates/next-core/src/next_image/module.rs | 42 ++-- .../next-swc/crates/next-core/src/router.rs | 14 +- .../implicit-metadata/input/app/favicon.ico | Bin 0 -> 15086 bytes .../implicit-metadata/input/app/icon1234.png | Bin 880 -> 6230 bytes .../input/app/manifest.webmanifest | 9 + .../input/app/opengraph-image.alt.txt | 1 + .../input/app/opengraph-image.png | Bin 0 -> 1661 bytes .../implicit-metadata/input/app/robots.txt | 2 + .../implicit-metadata/input/app/sitemap.xml | 7 + .../app/implicit-metadata/input/app/test.tsx | 79 +++++- ...rom filesystem is currently not-9116b0.txt | 21 -- 16 files changed, 624 insertions(+), 141 deletions(-) create mode 100644 packages/next-swc/crates/next-core/src/asset_helpers.rs create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/favicon.ico create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/manifest.webmanifest create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/opengraph-image.alt.txt create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/opengraph-image.png create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/robots.txt create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/sitemap.xml delete mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/issues/Implicit metadata from filesystem is currently not-9116b0.txt diff --git a/packages/next-swc/crates/napi/src/app_structure.rs b/packages/next-swc/crates/napi/src/app_structure.rs index 1a04522716f05..659f02288dd8b 100644 --- a/packages/next-swc/crates/napi/src/app_structure.rs +++ b/packages/next-swc/crates/napi/src/app_structure.rs @@ -8,7 +8,7 @@ use napi::{ }; use next_core::app_structure::{ find_app_dir, get_entrypoints as get_entrypoints_impl, Components, ComponentsVc, Entrypoint, - EntrypointsVc, LoaderTree, LoaderTreeVc, + EntrypointsVc, LoaderTree, LoaderTreeVc, MetadataWithAltItem, }; use serde::{Deserialize, Serialize}; use turbo_binding::{ @@ -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,59 +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(); - async fn add_meta( - meta: &mut serde_json::value::Map, + 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 Vec, project_path: FileSystemPathVc, - key: &str, - value: &Vec, + value: impl Iterator, ) -> Result<()> { - if !value.is_empty() { - meta.insert( - key.to_string(), - value - .iter() - .map(|value| async move { - Ok(serde_json::Value::from( - fs_path_to_path(project_path, *value).await?, - )) + let mut value = value.peekable(); + if value.peek().is_some() { + *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 } + } }) - .try_join() - .await? - .into(), - ); + }) + .try_join() + .await?; } Ok::<_, anyhow::Error>(()) } - add_meta(&mut meta, project_path, "icon", &metadata.icon).await?; - add_meta(&mut meta, project_path, "apple", &metadata.apple).await?; - add_meta(&mut meta, project_path, "twitter", &metadata.twitter).await?; - add_meta(&mut meta, project_path, "openGraph", &metadata.open_graph).await?; - add_meta(&mut meta, project_path, "favicon", &metadata.favicon).await?; - map.insert("metadata".to_string(), meta.into()); - Ok(map.into()) + 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.open_graph, + project_path, + metadata.open_graph.iter(), + ) + .await?; + 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 4eeb171f7d7b4..00c85d28c40ef 100644 --- a/packages/next-swc/crates/next-core/src/app_source.rs +++ b/packages/next-swc/crates/next-core/src/app_source.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, io::Write}; +use std::{collections::HashMap, io::Write, iter::once}; use anyhow::Result; use async_recursion::async_recursion; @@ -30,6 +30,7 @@ use turbo_binding::{ dev_server::{ html::DevHtmlAssetVc, source::{ + asset_graph::AssetGraphContentSourceVc, combined::CombinedContentSource, specificity::{Specificity, SpecificityElementType, SpecificityVc}, ContentSourceData, ContentSourceVc, NoContentSourceVc, @@ -37,6 +38,7 @@ use turbo_binding::{ }, ecmascript::{ magic_identifier, + text::TextContentSourceAssetVc, utils::{FormatIter, StringifyJs}, EcmascriptInputTransformsVc, EcmascriptModuleAssetType, EcmascriptModuleAssetVc, InnerAssetsVc, @@ -50,6 +52,7 @@ use turbo_binding::{ }, NodeEntry, NodeEntryVc, NodeRenderingEntry, NodeRenderingEntryVc, }, + r#static::{fixed::FixedStaticAssetVc, StaticModuleAssetVc}, turbopack::{ ecmascript::EcmascriptInputTransform, transition::{TransitionVc, TransitionsByNameVc}, @@ -62,8 +65,10 @@ use turbo_tasks::{TryJoinIterExt, ValueToString}; use crate::{ app_render::next_layout_entry_transition::NextServerComponentTransition, app_structure::{ - get_entrypoints, Components, Entrypoint, LoaderTree, LoaderTreeVc, Metadata, OptionAppDirVc, + get_entrypoints, get_global_metadata, Components, Entrypoint, GlobalMetadataVc, LoaderTree, + LoaderTreeVc, Metadata, MetadataItem, MetadataWithAltItem, OptionAppDirVc, }, + asset_helpers::as_es_module_asset, embed_js::{next_js_file, next_js_file_path}, env::env_for_js, fallback::get_fallback_page, @@ -85,6 +90,7 @@ use crate::{ context::{get_edge_compile_time_info, get_edge_resolve_options_context}, transition::NextEdgeTransition, }, + next_image::module::StructuredImageModuleType, next_route_matcher::NextParamsMatcherVc, next_server::context::{ get_server_compile_time_info, get_server_module_options_context, @@ -370,6 +376,7 @@ pub async fn create_app_source( return Ok(NoContentSourceVc::new().into()); }; let entrypoints = get_entrypoints(app_dir, next_config.page_extensions()); + let metadata = get_global_metadata(app_dir, next_config.page_extensions()); let client_compile_time_info = get_client_compile_time_info(browserslist_query); @@ -444,11 +451,58 @@ pub async fn create_app_source( output_path, ), }) + .chain(once(create_global_metadata_source( + app_dir, + metadata, + server_root, + ))) .collect(); Ok(CombinedContentSource { sources }.cell().into()) } +#[turbo_tasks::function] +async fn create_global_metadata_source( + app_dir: FileSystemPathVc, + metadata: GlobalMetadataVc, + server_root: FileSystemPathVc, +) -> Result { + let metadata = metadata.await?; + let mut unsupported_metadata = Vec::new(); + let mut sources = Vec::new(); + 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); + } + } + } + if !unsupported_metadata.is_empty() { + UnsupportedDynamicMetadataIssue { + app_dir, + files: unsupported_metadata, + } + .cell() + .as_issue() + .emit(); + } + Ok(CombinedContentSource { sources }.cell().into()) +} + #[allow(clippy::too_many_arguments)] #[turbo_tasks::function] async fn create_app_page_source_for_route( @@ -576,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, @@ -593,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!( @@ -622,10 +683,136 @@ import {}, {{ chunks as {} }} from "COMPONENT_{}"; Ok(()) } - fn emit_metadata_warning(state: &mut State, files: &[FileSystemPathVc]) { - for file in files { - state.unsupported_metadata.push(*file); + fn write_metadata(state: &mut State, metadata: &Metadata) -> Result<()> { + if metadata.is_empty() { + return Ok(()); } + let Metadata { + icon, + apple, + twitter, + open_graph, + favicon, + manifest, + } = metadata; + state.loader_tree_code += " metadata: {"; + write_metadata_items(state, "icon", favicon.iter().chain(icon.iter()))?; + write_metadata_items(state, "apple", apple.iter())?; + write_metadata_items(state, "twitter", twitter.iter())?; + write_metadata_items(state, "openGraph", open_graph.iter())?; + write_metadata_manifest(state, *manifest)?; + state.loader_tree_code += " },"; + Ok(()) + } + + fn write_metadata_manifest( + state: &mut State, + manifest: Option, + ) -> Result<()> { + let Some(manifest) = manifest else { + return Ok(()); + }; + match manifest { + MetadataItem::Static { path } => { + use std::fmt::Write; + let i = state.unique_number(); + let identifier = magic_identifier::mangle(&format!("manifest #{i}")); + let inner_module_id = format!("METADATA_{i}"); + state + .imports + .push(format!("import {identifier} from \"{inner_module_id}\";")); + state.inner_assets.insert( + inner_module_id, + StaticModuleAssetVc::new(SourceAssetVc::new(path).into(), state.context) + .into(), + ); + writeln!(state.loader_tree_code, " manifest: {identifier},")?; + } + MetadataItem::Dynamic { path } => { + state.unsupported_metadata.push(path); + } + } + + Ok(()) + } + + fn write_metadata_items<'a>( + state: &mut State, + name: &str, + it: impl Iterator, + ) -> Result<()> { + use std::fmt::Write; + let mut it = it.peekable(); + if it.peek().is_none() { + return Ok(()); + } + writeln!(state.loader_tree_code, " {name}: [")?; + for item in it { + write_metadata_item(state, name, item)?; + } + writeln!(state.loader_tree_code, " ],")?; + Ok(()) + } + + fn write_metadata_item( + state: &mut State, + name: &str, + item: &MetadataWithAltItem, + ) -> Result<()> { + use std::fmt::Write; + let i = state.unique_number(); + let identifier = magic_identifier::mangle(&format!("{name} #{i}")); + let inner_module_id = format!("METADATA_{i}"); + state + .imports + .push(format!("import {identifier} from \"{inner_module_id}\";")); + let s = " "; + match item { + MetadataWithAltItem::Static { path, alt_path } => { + state.inner_assets.insert( + inner_module_id, + StructuredImageModuleType::create_module( + SourceAssetVc::new(*path).into(), + state.context, + ) + .into(), + ); + writeln!(state.loader_tree_code, "{s}(async (props) => [{{")?; + writeln!(state.loader_tree_code, "{s} url: {identifier}.src,")?; + let numeric_sizes = name == "twitter" || name == "openGraph"; + if numeric_sizes { + writeln!(state.loader_tree_code, "{s} width: {identifier}.width,")?; + writeln!(state.loader_tree_code, "{s} height: {identifier}.height,")?; + } else { + writeln!( + state.loader_tree_code, + "{s} sizes: `${{{identifier}.width}}x${{{identifier}.height}}`," + )?; + } + if let Some(alt_path) = alt_path { + let identifier = magic_identifier::mangle(&format!("{name} alt text #{i}")); + let inner_module_id = format!("METADATA_ALT_{i}"); + state + .imports + .push(format!("import {identifier} from \"{inner_module_id}\";")); + state.inner_assets.insert( + inner_module_id, + as_es_module_asset( + TextContentSourceAssetVc::new(SourceAssetVc::new(*alt_path).into()) + .into(), + state.context, + ) + .into(), + ); + writeln!(state.loader_tree_code, "{s} alt: {identifier},")?; + } + writeln!(state.loader_tree_code, "{s}}}]),")?; + } + MetadataWithAltItem::Dynamic { path, .. } => { + state.unsupported_metadata.push(*path); + } + } + Ok(()) } #[async_recursion] @@ -658,14 +845,7 @@ import {}, {{ chunks as {} }} from "COMPONENT_{}"; layout, loading, template, - metadata: - Metadata { - icon, - apple, - twitter, - open_graph, - favicon, - }, + metadata, route: _, } = &*components.await?; write_component(state, "page", *page)?; @@ -674,12 +854,7 @@ import {}, {{ chunks as {} }} from "COMPONENT_{}"; write_component(state, "layout", *layout)?; write_component(state, "loading", *loading)?; write_component(state, "template", *template)?; - // TODO something useful for metadata - emit_metadata_warning(state, icon); - emit_metadata_warning(state, apple); - emit_metadata_warning(state, twitter); - emit_metadata_warning(state, open_graph); - emit_metadata_warning(state, favicon); + write_metadata(state, metadata)?; write!(state.loader_tree_code, "}}]")?; Ok(()) } @@ -695,7 +870,7 @@ import {}, {{ chunks as {} }} from "COMPONENT_{}"; } = state; if !unsupported_metadata.is_empty() { - UnsupportedImplicitMetadataIssue { + UnsupportedDynamicMetadataIssue { app_dir, files: unsupported_metadata, } @@ -877,13 +1052,13 @@ impl NodeEntry for AppRoute { } #[turbo_tasks::value] -struct UnsupportedImplicitMetadataIssue { +struct UnsupportedDynamicMetadataIssue { app_dir: FileSystemPathVc, files: Vec, } #[turbo_tasks::value_impl] -impl Issue for UnsupportedImplicitMetadataIssue { +impl Issue for UnsupportedDynamicMetadataIssue { #[turbo_tasks::function] fn severity(&self) -> IssueSeverityVc { IssueSeverity::Warning.into() @@ -902,7 +1077,7 @@ impl Issue for UnsupportedImplicitMetadataIssue { #[turbo_tasks::function] fn title(&self) -> StringVc { StringVc::cell( - "Implicit metadata from filesystem is currently not supported in Turbopack".to_string(), + "Dynamic metadata from filesystem is currently not supported in Turbopack".to_string(), ) } 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 5f59531b39507..bf3ed493ac860 100644 --- a/packages/next-swc/crates/next-core/src/app_structure.rs +++ b/packages/next-swc/crates/next-core/src/app_structure.rs @@ -82,18 +82,40 @@ 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 { + path: FileSystemPathVc, + alt_path: Option, + }, + Dynamic { + path: FileSystemPathVc, + }, +} + +/// 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")] - pub icon: Vec, + pub icon: Vec, #[serde(skip_serializing_if = "Vec::is_empty")] - pub apple: Vec, + pub apple: Vec, #[serde(skip_serializing_if = "Vec::is_empty")] - pub twitter: Vec, + pub twitter: Vec, #[serde(skip_serializing_if = "Vec::is_empty")] - pub open_graph: Vec, + pub open_graph: Vec, #[serde(skip_serializing_if = "Vec::is_empty")] - pub favicon: Vec, + pub favicon: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub manifest: Option, } impl Metadata { @@ -104,12 +126,14 @@ impl Metadata { twitter, open_graph, favicon, + manifest, } = self; icon.is_empty() && apple.is_empty() && twitter.is_empty() && open_graph.is_empty() && favicon.is_empty() + && manifest.is_none() } fn merge(a: &Self, b: &Self) -> Self { @@ -124,10 +148,34 @@ impl Metadata { .copied() .collect(), favicon: a.favicon.iter().chain(b.favicon.iter()).copied().collect(), + manifest: a.manifest.or(b.manifest), } } } +/// Metadata files that can be placed in the root of the app directory. +#[turbo_tasks::value] +#[derive(Default, Clone, Debug)] +pub struct GlobalMetadata { + #[serde(skip_serializing_if = "Option::is_none")] + pub favicon: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub robots: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sitemap: Option, +} + +impl GlobalMetadata { + pub fn is_empty(&self) -> bool { + let GlobalMetadata { + favicon, + robots, + sitemap, + } = self; + favicon.is_none() && robots.is_none() && sitemap.is_none() + } +} + #[turbo_tasks::value] #[derive(Debug)] pub struct DirectoryTree { @@ -203,7 +251,7 @@ pub async fn find_app_dir_if_enabled( Ok(find_app_dir(project_path)) } -static STATIC_METADATA_IMAGES: Lazy> = +static STATIC_LOCAL_METADATA: Lazy> = Lazy::new(|| { HashMap::from([ ( @@ -211,18 +259,36 @@ static STATIC_METADATA_IMAGES: Lazy Option<&'static str> { +static STATIC_GLOBAL_METADATA: Lazy> = + Lazy::new(|| { + HashMap::from([ + ("favicon", &["ico"] as &'static [&'static str]), + ("robots", &["txt"]), + ("sitemap", &["xml"]), + ]) + }); + +fn match_metadata_file<'a>( + basename: &'a str, + page_extensions: &[String], +) -> Option<(&'a str, i32, bool)> { let (stem, ext) = basename.split_once('.')?; - static REGEX: Lazy = Lazy::new(|| Regex::new("\\d*$").unwrap()); - let stem = REGEX.replace(stem, ""); - let (key, exts) = STATIC_METADATA_IMAGES.get_key_value(stem.as_ref())?; - exts.contains(&ext).then_some(key) + static REGEX: Lazy = Lazy::new(|| Regex::new("^(.*?)(\\d*)$").unwrap()); + let captures = REGEX.captures(stem).expect("the regex will always match"); + let stem = captures.get(1).unwrap().as_str(); + let num: i32 = captures.get(2).unwrap().as_str().parse().unwrap_or(-1); + if page_extensions.iter().any(|e| e == ext) { + return Some((stem, num, true)); + } + let exts = STATIC_LOCAL_METADATA.get(stem)?; + exts.contains(&ext).then_some((stem, num, false)) } #[turbo_tasks::function] @@ -233,14 +299,22 @@ async fn get_directory_tree( let DirectoryContent::Entries(entries) = &*app_dir.read_dir().await? else { bail!("app_dir must be a directory") }; + let page_extensions_value = page_extensions.await?; + let mut subdirectories = BTreeMap::new(); let mut components = Components::default(); + let mut metadata_icon = Vec::new(); + let mut metadata_apple = Vec::new(); + let mut metadata_open_graph = Vec::new(); + let mut metadata_twitter = Vec::new(); + let mut metadata_favicon = Vec::new(); + for (basename, entry) in entries { match *entry { DirectoryEntry::File(file) => { if let Some((stem, ext)) = basename.split_once('.') { - if page_extensions.await?.iter().any(|e| e == ext) { + if page_extensions_value.iter().any(|e| e == ext) { match stem { "page" => components.page = Some(file), "layout" => components.layout = Some(file), @@ -249,24 +323,57 @@ async fn get_directory_tree( "template" => components.template = Some(file), "default" => components.default = Some(file), "route" => components.route = Some(file), + "manifest" => { + components.metadata.manifest = + Some(MetadataItem::Dynamic { path: file }); + continue; + } _ => {} } } } - if let Some(metadata_type) = match_metadata_file(basename.as_str()) { - let metadata = &mut components.metadata; + + if let Some((metadata_type, num, dynamic)) = + match_metadata_file(basename.as_str(), &page_extensions_value) + { + if metadata_type == "manifest" { + if num == -1 { + components.metadata.manifest = + Some(MetadataItem::Static { path: file }); + } + continue; + } let entry = match metadata_type { - "icon" => Some(&mut metadata.icon), - "apple-icon" => Some(&mut metadata.apple), - "twitter-image" => Some(&mut metadata.twitter), - "opengraph-image" => Some(&mut metadata.open_graph), - "favicon" => Some(&mut metadata.favicon), + "icon" => Some(&mut metadata_icon), + "apple-icon" => Some(&mut metadata_apple), + "twitter-image" => Some(&mut metadata_twitter), + "opengraph-image" => Some(&mut metadata_open_graph), + "favicon" => Some(&mut metadata_favicon), _ => None, }; if let Some(entry) = entry { - entry.push(file) + if dynamic { + entry.push((num, MetadataWithAltItem::Dynamic { path: file })); + } else { + let file_value = file.await?; + let file_name = file_value.file_name(); + let basename = file_name + .rsplit_once('.') + .map_or(file_name, |(basename, _)| basename); + let alt_path = file.parent().join(&format!("{}.alt.txt", basename)); + let alt_path = + matches!(&*alt_path.get_type().await?, FileSystemEntryType::File) + .then_some(alt_path); + entry.push(( + num, + MetadataWithAltItem::Static { + path: file, + alt_path, + }, + )); + } } } } @@ -274,11 +381,22 @@ 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 _ => {} } } + fn sort(mut list: Vec<(i32, T)>) -> Vec { + list.sort_by_key(|(num, _)| *num); + list.into_iter().map(|(_, item)| item).collect() + } + + components.metadata.icon = sort(metadata_icon); + components.metadata.apple = sort(metadata_apple); + components.metadata.twitter = sort(metadata_twitter); + components.metadata.open_graph = sort(metadata_open_graph); + components.metadata.favicon = sort(metadata_favicon); + Ok(DirectoryTree { subdirectories, components: components.cell(), @@ -576,6 +694,42 @@ 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, + page_extensions: StringsVc, +) -> Result { + let DirectoryContent::Entries(entries) = &*app_dir.read_dir().await? else { + bail!("app_dir must be a directory") + }; + let mut metadata = GlobalMetadata::default(); + + for (basename, entry) in entries { + if let DirectoryEntry::File(file) = *entry { + if let Some((stem, ext)) = basename.split_once('.') { + let list = match stem { + "favicon" => Some(&mut metadata.favicon), + "sitemap" => Some(&mut metadata.sitemap), + "robots" => Some(&mut metadata.robots), + _ => None, + }; + if let Some(list) = list { + if page_extensions.await?.iter().any(|e| e == ext) { + *list = Some(MetadataItem::Dynamic { path: file }); + } + if STATIC_GLOBAL_METADATA.get(stem).unwrap().contains(&ext) { + *list = Some(MetadataItem::Static { path: file }); + } + } + } + } + // TODO(WEB-952) handle symlinks in app dir + } + + Ok(metadata.cell()) +} + #[turbo_tasks::value(shared)] struct DirectoryTreeIssue { pub severity: IssueSeverityVc, diff --git a/packages/next-swc/crates/next-core/src/asset_helpers.rs b/packages/next-swc/crates/next-core/src/asset_helpers.rs new file mode 100644 index 0000000000000..6dc253261d4f7 --- /dev/null +++ b/packages/next-swc/crates/next-core/src/asset_helpers.rs @@ -0,0 +1,27 @@ +use turbo_binding::turbopack::{ + core::{ + asset::AssetVc, + context::{AssetContext, AssetContextVc}, + }, + ecmascript::{ + EcmascriptInputTransform, EcmascriptInputTransformsVc, EcmascriptModuleAssetType, + EcmascriptModuleAssetVc, + }, +}; +use turbo_tasks::Value; + +pub(crate) fn as_es_module_asset( + asset: AssetVc, + context: AssetContextVc, +) -> EcmascriptModuleAssetVc { + EcmascriptModuleAssetVc::new( + asset, + context, + Value::new(EcmascriptModuleAssetType::Typescript), + EcmascriptInputTransformsVc::cell(vec![EcmascriptInputTransform::TypeScript { + use_define_for_class_fields: false, + }]), + Default::default(), + context.compile_time_info(), + ) +} diff --git a/packages/next-swc/crates/next-core/src/lib.rs b/packages/next-swc/crates/next-core/src/lib.rs index fa249e39fd220..b2f58717de922 100644 --- a/packages/next-swc/crates/next-core/src/lib.rs +++ b/packages/next-swc/crates/next-core/src/lib.rs @@ -5,6 +5,7 @@ mod app_render; mod app_source; pub mod app_structure; +mod asset_helpers; mod babel; mod embed_js; pub mod env; diff --git a/packages/next-swc/crates/next-core/src/next_image/module.rs b/packages/next-swc/crates/next-core/src/next_image/module.rs index 9c49905e52370..61a8672f5e197 100644 --- a/packages/next-swc/crates/next-core/src/next_image/module.rs +++ b/packages/next-swc/crates/next-core/src/next_image/module.rs @@ -24,25 +24,13 @@ use super::source_asset::StructuredImageSourceAsset; #[turbo_tasks::value] pub struct StructuredImageModuleType {} -#[turbo_tasks::value_impl] -impl StructuredImageModuleTypeVc { - #[turbo_tasks::function] - pub fn new() -> Self { - StructuredImageModuleTypeVc::cell(StructuredImageModuleType {}) - } -} - -#[turbo_tasks::value_impl] -impl CustomModuleType for StructuredImageModuleType { - #[turbo_tasks::function] - async fn create_module( - &self, +impl StructuredImageModuleType { + pub(crate) fn create_module( source: AssetVc, context: AssetContextVc, - _part: Option, - ) -> Result { + ) -> EcmascriptModuleAssetVc { let static_asset = StaticModuleAssetVc::new(source, context); - Ok(EcmascriptModuleAssetVc::new_with_inner_assets( + EcmascriptModuleAssetVc::new_with_inner_assets( StructuredImageSourceAsset { image: source }.cell().into(), context, Value::new(EcmascriptModuleAssetType::Ecmascript), @@ -55,6 +43,26 @@ impl CustomModuleType for StructuredImageModuleType { "IMAGE".to_string() => static_asset.into() )), ) - .into()) + } +} + +#[turbo_tasks::value_impl] +impl StructuredImageModuleTypeVc { + #[turbo_tasks::function] + pub fn new() -> Self { + StructuredImageModuleTypeVc::cell(StructuredImageModuleType {}) + } +} + +#[turbo_tasks::value_impl] +impl CustomModuleType for StructuredImageModuleType { + #[turbo_tasks::function] + fn create_module( + &self, + source: AssetVc, + context: AssetContextVc, + _part: Option, + ) -> AssetVc { + StructuredImageModuleType::create_module(source, context).into() } } diff --git a/packages/next-swc/crates/next-core/src/router.rs b/packages/next-swc/crates/next-core/src/router.rs index c24f18206afa9..f4c6e2b73dc62 100644 --- a/packages/next-swc/crates/next-core/src/router.rs +++ b/packages/next-swc/crates/next-core/src/router.rs @@ -45,6 +45,7 @@ use turbo_tasks::{ use turbo_tasks_fs::json::parse_json_with_source_context; use crate::{ + asset_helpers::as_es_module_asset, embed_js::{next_asset, next_js_file}, next_config::NextConfigVc, next_edge::{ @@ -156,19 +157,6 @@ async fn get_config( Ok(OptionEcmascriptModuleAssetVc::cell(config_asset)) } -fn as_es_module_asset(asset: AssetVc, context: AssetContextVc) -> EcmascriptModuleAssetVc { - EcmascriptModuleAssetVc::new( - asset, - context, - Value::new(EcmascriptModuleAssetType::Typescript), - EcmascriptInputTransformsVc::cell(vec![EcmascriptInputTransform::TypeScript { - use_define_for_class_fields: false, - }]), - Default::default(), - context.compile_time_info(), - ) -} - #[turbo_tasks::function] async fn next_config_changed( context: AssetContextVc, diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/favicon.ico b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4965832f2c9b0605eaa189b7c7fb11124d24e48a GIT binary patch literal 15086 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 CmjnF( delta 852 zcmca+@PVzqGr-TCmrII^fq{Y7)59eQNGpIa2MdtAS-hzpNU@|l`Z_W&Z0zU$lgJ8^ zO!f%!WnidMV_;}#VPNps$QT@CsApgj^>lFz@i;zr>e=kzK!Kz6JN31;Ze7vactp=d zU`f~t4?CBbB~BmmCpfu^2#UPA_sGdb=+3k>WvB8crFSk zgvr+9u2FA)`^|~GTRG=*+Pk@y!UuB${R$?^Ece_X|HHA__0*oFr_>bFCN6a8H7lyG zm@TDh6yf+m#WgJU!rgsd6PL)_9A%1bEKuAQq41RT{6-nBdmmGa{3>R7T`y47TJm=5 z`R^>^N&oK0N^sd8Zq-PzVu_sf-}QnDH(R&oqusAN3py6ce#(_jY1*UgHpf#(Vqsc( z-|MM%FPe0xT`J#uoiUxy)Q#sc&+Sfji=&e7>IFBOu5g{w@%e3=r1_JMs_jR4%@5d` zS{`0uE%#7d`p&Z&3C12Np*R*^FCN3Q4-J}H7T(v^oA+Uo&Go|^zk17U4q1I)X?%32 z0Gsn^Yv#l5Tt{X+OAy$Xn(?E4?qkd8JJ@QH&;It6YExa$)Y2fvAMubcbJdv(-;^SZ zrZku2O>D21d8pTH^K5JMSu@S*EuRW&uXgq2&HrVx{QfMVuFUoB+RaDT*w*;OhlDE?3D+DkLuC%vq{fLp^cl~mlf@WyL|%ytmV9zDoeI>;;OXk; Jvd$@?2>_grTz&um diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/manifest.webmanifest b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/manifest.webmanifest new file mode 100644 index 0000000000000..f3e287945395b --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/manifest.webmanifest @@ -0,0 +1,9 @@ +{ + "name": "Next.js Static Manifest", + "short_name": "Next.js App", + "description": "Next.js App", + "start_url": "/", + "display": "standalone", + "background_color": "#fff", + "theme_color": "#fff" +} 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 new file mode 100644 index 0000000000000..e07d9e8a67033 --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/opengraph-image.alt.txt @@ -0,0 +1 @@ +This is an alt text. \ No newline at end of file diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/opengraph-image.png b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/opengraph-image.png new file mode 100644 index 0000000000000000000000000000000000000000..7cbc1d26733614b83dcc25b9cfcd5e06dffb147c GIT binary patch literal 1661 zcmV-@27>vCP)LUUqAigYBsiM4LpbwXrcM#DLg%sv zqYV7atJ2!&9+4a}Z~>mx*QQG8O{j2%jKM&gp$l9QGmOhXo1ym@G1NiGAj_1lhANDV z1!S4frxq9i3di1^l$}15Gz3>C_2kBUE^I5>7jp>X5wdo&Pd!2a6f5GP)GgXa3QSQ5 zJ0>bk8Vt=;h$f0RQz4ot-b{sPqIfeEqKV?oREQ=HW#$APi4kKst#JY0qCFI7KO!;| z-{Dx5|6)LB*iF*ATe1cl&QpkEr#`hqi_TA*>5v*DyuQX9nwI%PlNUBUM=lW0iy@%% zB^BF?>xE4!QsG#Qk#JB|Zt7m}4cuf9&Sc^>nhincOJ6g{>L6IPUat?vOi^vN=pG%= zK#)fFriX`z1CgewsCHXKz1WKmEejx6qkEB3>Y%mGjoj^a`R9__Z&8YtD2$*QSF6?G zlq}0cwcn!FRXGQOH@cIk`fMagBI@t3C`Ai25VWzK_5A#7HS(IOas~u%Y$wfTvqK&8 z`CQcBV^O1VSn-#r$_`Ey)bSA>M>05=GHK|=D&vNW5yX)MHLMsk@M4|D4HqMbBTuM7 zGiqpSoyQFq!aF`YRFE)gWNTH$4Hv@WNCFEhW{qs6s<<)osDkweD~7ZT#gP4NM1Ob2K1o@yQY z{T_FBclnkpP431DijTPALP{KAd${l!H(UsaBYd9=;5%-(VBYcJ89zeB4Hrz~h-Q0^ ze0=2jeA>lg0fywgunf%JVWH!O3x;tdBO_PL8pWoAQw77TBTG88Zovo+al-|}I6`Y+ z^0|$UQ`~UDEROt4MzXmBi(A}q!6=R-WF(8pS6t(U3np>o%N@)*bYntY-RKH~s(kZ& z*Hy^IaE}`<0>_c%sgbjQ4H479sUmP3xhDh5k)0O=LPFed5jc*#o_|5Z^Sen>+;9;n zj%ZkT@@=HFxZxr|$7g9XGV$36sd2-F=Qxs}0&mwQrN#{x9;;HL1}}$K>2bq_r#P}a zHSYXT*aJ@Mua=>9?N1|Wq^)( z8lD&1iZo_xba(zmTSe0Rk3SIMiq)H{bw6TN@?>lD0hU=ERkP?~Rr2IdR7}mOTZt!D zB`>zHoEuB(=Cx8){)z|}QgkGPtc*2AX*g)I$Qez}x@fuBNN>;~bzAp>Cd&@~hoj*M zA|d$Q=r7lc+AIW0RXIh(CtK8Xa87A+6VERwZWf7nf%f%>rY~-@>N)-3|BZe^#0S3_ z{euDVJ|*M|Rl5{KpJV_IQG>{9LMTUlr0g6i(g^TuK!s?gcoRz$qM71NEK!JNiZ`)D zA(|=P#1e&Qrg#%e6h^R+s0U>ba-BC08dS?$*zXIv&y4Nk(E@0PhHE2pL?NCxNoxpO z1fYr(vUYr*N-;7NuB$pTjQslT4C6MG&V~25B2tXY&|UF2UFGyqg=0@nLzZdLz#3+d zHJFeYJs=ub33U)w;8~?qeH7>uL|Ty`ys7lN*_+p9I%H@Rjysd|J%;PrZ*hEp)N8Tb zLI9GCJcrIr)06Ejj%H5^Da-fl@pVe)w$P_yyD7FJr+j_}po~WmWUki$00000NkvXX Hu0mjfdKc_u literal 0 HcmV?d00001 diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/robots.txt b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/robots.txt new file mode 100644 index 0000000000000..0ad279c73692e --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/robots.txt @@ -0,0 +1,2 @@ +User-Agent: * +Disallow: diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/sitemap.xml b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/sitemap.xml new file mode 100644 index 0000000000000..9d2b2deade0c4 --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/sitemap.xml @@ -0,0 +1,7 @@ + + + + https://vercel.com/ + 2023-03-06T18:04:14.008Z + + diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/test.tsx b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/test.tsx index f4994b194885f..2633ad971ff98 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/test.tsx +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/test.tsx @@ -5,7 +5,84 @@ import { useEffect } from 'react' export default function Test() { useEffect(() => { import('@turbo/pack-test-harness').then(() => { - it('should run', () => {}) + it('should have the correct link tags', () => { + let links = Array.from(document.querySelectorAll('link')) + expect( + links.map((l) => ({ + href: l.getAttribute('href'), + rel: l.getAttribute('rel'), + sizes: l.getAttribute('sizes'), + })) + ).toEqual([ + expect.objectContaining({ + rel: 'manifest', + href: expect.stringMatching(/^\/_next\/static\/.+\.webmanifest$/), + sizes: null, + }), + expect.objectContaining({ + rel: 'icon', + href: expect.stringMatching(/^\/_next\/static\/.+\.ico$/), + sizes: '48x48', + }), + expect.objectContaining({ + rel: 'icon', + href: expect.stringMatching(/^\/_next\/static\/.+\.png$/), + sizes: '32x32', + }), + expect.objectContaining({ + rel: 'icon', + href: expect.stringMatching(/^\/_next\/static\/.+\.png$/), + sizes: '64x64', + }), + expect.objectContaining({ + rel: 'apple-touch-icon', + href: expect.stringMatching(/^\/_next\/static\/.+\.png$/), + sizes: '114x114', + }), + ]) + }) + + it('should have the correct meta tags', () => { + const meta = Array.from(document.querySelectorAll('meta')) + const metaObject = Object.fromEntries( + meta + .filter((l) => l.getAttribute('property')) + .map((l) => [l.getAttribute('property'), l.getAttribute('content')]) + ) + expect(metaObject).toEqual({ + 'og:image': expect.stringMatching(/^.+\/_next\/static\/.+\.png$/), + 'og:image:width': '114', + 'og:image:height': '114', + 'og:image:alt': 'This is an alt text.', + }) + }) + + it('should provide a robots.txt', async () => { + const res = await fetch('/robots.txt') + expect(res.status).toBe(200) + expect(await res.text()).toBe('User-Agent: *\nDisallow:\n') + }) + + it('should provide a sitemap.xml', async () => { + const res = await fetch('/sitemap.xml') + expect(res.status).toBe(200) + expect(await res.text()).toBe( + ` + + + https://vercel.com/ + 2023-03-06T18:04:14.008Z + + +` + ) + }) + + it('should provide a favicon.ico', async () => { + const res = await fetch('/favicon.ico') + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toBe('image/x-icon') + }) }) return () => {} }, []) diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/issues/Implicit metadata from filesystem is currently not-9116b0.txt b/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/issues/Implicit metadata from filesystem is currently not-9116b0.txt deleted file mode 100644 index bc808b91de05f..0000000000000 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/issues/Implicit metadata from filesystem is currently not-9116b0.txt +++ /dev/null @@ -1,21 +0,0 @@ -PlainIssue { - severity: Warning, - context: "[project]/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app", - category: "unsupported", - title: "Implicit metadata from filesystem is currently not supported in Turbopack", - description: "The following files were found in the app directory, but are not supported by Turbopack. They are ignored:\n\n- [project]/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/apple-icon.png\n- [project]/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/icon1234.png\n- [project]/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app/icon234.png", - detail: "", - documentation_link: "", - source: None, - sub_issues: [], - processing_path: Some( - [ - PlainIssueProcessingPathItem { - context: Some( - "[project]/packages/next-swc/crates/next-dev-tests/tests/integration/next/app/implicit-metadata/input/app", - ), - description: "Next.js App Page Route /", - }, - ], - ), -} \ No newline at end of file