feat(turbopack): add dynamic metadata support (#54995)
Closes NEXT-1435 Closes WEB-1435
This commit is contained in:
parent
f569cb1316
commit
b5d752667a
21 changed files with 1772 additions and 539 deletions
170
Cargo.lock
generated
170
Cargo.lock
generated
|
@ -133,6 +133,54 @@ dependencies = [
|
|||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "any_ascii"
|
||||
version = "0.1.7"
|
||||
|
@ -925,30 +973,36 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.1.11"
|
||||
version = "4.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42dfd32784433290c51d92c438bb72ea5063797fc3cc9a21a8c4346bebbb2098"
|
||||
checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6"
|
||||
dependencies = [
|
||||
"bitflags 2.3.3",
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
"clap_lex 0.3.3",
|
||||
"is-terminal",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex 0.5.1",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.1.9"
|
||||
version = "4.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fddf67631444a3a3e3e5ac51c36a5e01335302de677bd78759eaa90ab1f46644"
|
||||
checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.25",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -962,12 +1016,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.3.3"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961"
|
||||
|
||||
[[package]]
|
||||
name = "cloudabi"
|
||||
|
@ -1000,6 +1051,12 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.6"
|
||||
|
@ -1824,12 +1881,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.9.3"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7"
|
||||
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"humantime",
|
||||
"is-terminal",
|
||||
"log",
|
||||
"regex",
|
||||
"termcolor",
|
||||
|
@ -2431,15 +2488,15 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
|||
|
||||
[[package]]
|
||||
name = "httpmock"
|
||||
version = "0.6.7"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6b56b6265f15908780cbee987912c1e98dbca675361f748291605a8a3a1df09"
|
||||
checksum = "4b02e044d3b4c2f94936fb05f9649efa658ca788f44eb6b87554e2033fc8ce93"
|
||||
dependencies = [
|
||||
"assert-json-diff",
|
||||
"async-object-pool",
|
||||
"async-trait",
|
||||
"base64 0.13.1",
|
||||
"clap 4.1.11",
|
||||
"base64 0.21.0",
|
||||
"clap 4.4.2",
|
||||
"crossbeam-utils",
|
||||
"env_logger",
|
||||
"form_urlencoded",
|
||||
|
@ -2869,6 +2926,29 @@ dependencies = [
|
|||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy-regex"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57451d19ad5e289ff6c3d69c2a2424652995c42b79dafa11e9c4d5508c913c01"
|
||||
dependencies = [
|
||||
"lazy-regex-proc_macros",
|
||||
"once_cell",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy-regex-proc_macros"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f0a1d9139f0ee2e862e08a9c5d0ba0470f2aa21cd1e1aa1b1562f83116c725f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.25",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
|
@ -3171,7 +3251,7 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
"regex-automata 0.1.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3547,7 +3627,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"async-recursion",
|
||||
"base64 0.21.0",
|
||||
"clap 4.1.11",
|
||||
"clap 4.4.2",
|
||||
"console-subscriber",
|
||||
"dunce",
|
||||
"indexmap 1.9.3",
|
||||
|
@ -3577,6 +3657,7 @@ dependencies = [
|
|||
"futures",
|
||||
"indexmap 1.9.3",
|
||||
"indoc",
|
||||
"lazy-regex",
|
||||
"lazy_static",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
|
@ -3601,7 +3682,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"chromiumoxide",
|
||||
"clap 4.1.11",
|
||||
"clap 4.4.2",
|
||||
"console-subscriber",
|
||||
"criterion",
|
||||
"dunce",
|
||||
|
@ -4747,13 +4828,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.8.3"
|
||||
version = "1.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390"
|
||||
checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.7.2",
|
||||
"regex-automata 0.3.8",
|
||||
"regex-syntax 0.7.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4765,6 +4847,17 @@ dependencies = [
|
|||
"regex-syntax 0.6.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.7.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.29"
|
||||
|
@ -4773,9 +4866,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
|||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.2"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
|
||||
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
||||
|
||||
[[package]]
|
||||
name = "region"
|
||||
|
@ -7168,11 +7261,12 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.28.2"
|
||||
version = "1.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2"
|
||||
checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"backtrace",
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio 0.8.6",
|
||||
|
@ -7819,7 +7913,7 @@ version = "0.1.0"
|
|||
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230901.3#d76ad75b0402cd9f9583ffba8885fc688e012a03"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.1.11",
|
||||
"clap 4.4.2",
|
||||
"crossbeam-channel",
|
||||
"crossterm",
|
||||
"once_cell",
|
||||
|
@ -7872,7 +7966,7 @@ version = "0.1.0"
|
|||
source = "git+https://github.com/vercel/turbo.git?tag=turbopack-230901.3#d76ad75b0402cd9f9583ffba8885fc688e012a03"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.1.11",
|
||||
"clap 4.4.2",
|
||||
"indoc",
|
||||
"pathdiff",
|
||||
"serde_json",
|
||||
|
@ -8387,6 +8481,12 @@ version = "0.7.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.3.3"
|
||||
|
|
|
@ -16,9 +16,6 @@ members = [
|
|||
"packages/next-swc/crates/next-transform-strip-page-exports",
|
||||
]
|
||||
|
||||
[profile.dev.package.swc_css_prefixer]
|
||||
opt-level = 2
|
||||
|
||||
# This is a workaround for wasm timeout issue
|
||||
[profile.dev.package."*"]
|
||||
debug-assertions = false
|
||||
|
|
|
@ -8,7 +8,7 @@ use napi::{
|
|||
};
|
||||
use next_core::app_structure::{
|
||||
find_app_dir, get_entrypoints as get_entrypoints_impl, Components, Entrypoint, Entrypoints,
|
||||
LoaderTree, MetadataWithAltItem,
|
||||
LoaderTree, MetadataItem, MetadataWithAltItem,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use turbo_tasks::{ReadRef, Vc};
|
||||
|
@ -41,6 +41,8 @@ struct LoaderTreeForJs {
|
|||
parallel_routes: HashMap<String, ReadRef<LoaderTreeForJs>>,
|
||||
#[turbo_tasks(trace_ignore)]
|
||||
components: ComponentsForJs,
|
||||
#[turbo_tasks(trace_ignore)]
|
||||
global_metadata: GlobalMetadataForJs,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Serialize, Deserialize, ValueDebugFormat, TraceRawVcs)]
|
||||
|
@ -101,20 +103,31 @@ struct ComponentsForJs {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
struct MetadataForJs {
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
icon: Vec<MetadataForJsItem>,
|
||||
icon: Vec<MetadataWithAltItemForJs>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
apple: Vec<MetadataForJsItem>,
|
||||
apple: Vec<MetadataWithAltItemForJs>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
twitter: Vec<MetadataForJsItem>,
|
||||
twitter: Vec<MetadataWithAltItemForJs>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
open_graph: Vec<MetadataForJsItem>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
favicon: Vec<MetadataForJsItem>,
|
||||
open_graph: Vec<MetadataWithAltItemForJs>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
sitemap: Option<MetadataItemForJs>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, ValueDebugFormat)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct GlobalMetadataForJs {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
favicon: Option<MetadataItemForJs>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
robots: Option<MetadataItemForJs>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
manifest: Option<MetadataItemForJs>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, ValueDebugFormat)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
enum MetadataForJsItem {
|
||||
enum MetadataWithAltItemForJs {
|
||||
Static {
|
||||
path: String,
|
||||
alt_path: Option<String>,
|
||||
|
@ -124,6 +137,13 @@ enum MetadataForJsItem {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, ValueDebugFormat)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
enum MetadataItemForJs {
|
||||
Static { path: String },
|
||||
Dynamic { path: String },
|
||||
}
|
||||
|
||||
async fn prepare_components_for_js(
|
||||
project_path: Vc<FileSystemPath>,
|
||||
components: Vc<Components>,
|
||||
|
@ -158,60 +178,88 @@ async fn prepare_components_for_js(
|
|||
add(&mut result.not_found, project_path, not_found).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<MetadataForJsItem>,
|
||||
project_path: Vc<FileSystemPath>,
|
||||
value: impl Iterator<Item = &'a MetadataWithAltItem>,
|
||||
) -> Result<()> {
|
||||
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?;
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}
|
||||
|
||||
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(
|
||||
add_meta_vec(&mut meta.icon, project_path, metadata.icon.iter()).await?;
|
||||
add_meta_vec(&mut meta.apple, project_path, metadata.apple.iter()).await?;
|
||||
add_meta_vec(&mut meta.twitter, project_path, metadata.twitter.iter()).await?;
|
||||
add_meta_vec(
|
||||
&mut meta.open_graph,
|
||||
project_path,
|
||||
metadata.open_graph.iter(),
|
||||
)
|
||||
.await?;
|
||||
add_meta(&mut meta.favicon, project_path, metadata.favicon.iter()).await?;
|
||||
add_meta(&mut meta.sitemap, project_path, metadata.sitemap).await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn add_meta_vec<'a>(
|
||||
meta: &mut Vec<MetadataWithAltItemForJs>,
|
||||
project_path: Vc<FileSystemPath>,
|
||||
value: impl Iterator<Item = &'a MetadataWithAltItem>,
|
||||
) -> Result<()> {
|
||||
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
|
||||
};
|
||||
MetadataWithAltItemForJs::Static { path, alt_path }
|
||||
}
|
||||
MetadataWithAltItem::Dynamic { path } => {
|
||||
let path = fs_path_to_path(project_path, *path).await?;
|
||||
MetadataWithAltItemForJs::Dynamic { path }
|
||||
}
|
||||
})
|
||||
})
|
||||
.try_join()
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn add_meta<'a>(
|
||||
meta: &mut Option<MetadataItemForJs>,
|
||||
project_path: Vc<FileSystemPath>,
|
||||
value: Option<MetadataItem>,
|
||||
) -> Result<()> {
|
||||
if value.is_some() {
|
||||
*meta = match value {
|
||||
Some(MetadataItem::Static { path }) => {
|
||||
let path = fs_path_to_path(project_path, path).await?;
|
||||
Some(MetadataItemForJs::Static { path })
|
||||
}
|
||||
Some(MetadataItem::Dynamic { path }) => {
|
||||
let path = fs_path_to_path(project_path, path).await?;
|
||||
Some(MetadataItemForJs::Dynamic { path })
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn prepare_loader_tree_for_js(
|
||||
project_path: Vc<FileSystemPath>,
|
||||
loader_tree: Vc<LoaderTree>,
|
||||
) -> Result<Vc<LoaderTreeForJs>> {
|
||||
let LoaderTree {
|
||||
page: _,
|
||||
segment,
|
||||
parallel_routes,
|
||||
components,
|
||||
global_metadata,
|
||||
} = &*loader_tree.await?;
|
||||
|
||||
let parallel_routes = parallel_routes
|
||||
.iter()
|
||||
.map(|(key, &value)| async move {
|
||||
|
@ -224,11 +272,21 @@ async fn prepare_loader_tree_for_js(
|
|||
.await?
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let components = prepare_components_for_js(project_path, *components).await?;
|
||||
|
||||
let global_metadata = global_metadata.await?;
|
||||
|
||||
let mut meta = GlobalMetadataForJs::default();
|
||||
add_meta(&mut meta.favicon, project_path, global_metadata.favicon).await?;
|
||||
add_meta(&mut meta.manifest, project_path, global_metadata.manifest).await?;
|
||||
add_meta(&mut meta.robots, project_path, global_metadata.robots).await?;
|
||||
|
||||
Ok(LoaderTreeForJs {
|
||||
segment: segment.clone(),
|
||||
parallel_routes,
|
||||
components,
|
||||
global_metadata: meta,
|
||||
}
|
||||
.cell())
|
||||
}
|
||||
|
@ -251,6 +309,9 @@ async fn prepare_entrypoints_for_js(
|
|||
Entrypoint::AppRoute { path, .. } => EntrypointForJs::AppRoute {
|
||||
path: fs_path_to_path(project_path, path).await?,
|
||||
},
|
||||
Entrypoint::AppMetadata { metadata, .. } => EntrypointForJs::AppRoute {
|
||||
path: fs_path_to_path(project_path, metadata.into_path()).await?,
|
||||
},
|
||||
};
|
||||
Ok((key, value))
|
||||
}
|
||||
|
|
|
@ -3,12 +3,13 @@ use next_core::{
|
|||
all_server_paths,
|
||||
app_structure::{
|
||||
get_entrypoints, Entrypoint as AppEntrypoint, Entrypoints as AppEntrypoints, LoaderTree,
|
||||
MetadataItem,
|
||||
},
|
||||
get_edge_resolve_options_context,
|
||||
mode::NextMode,
|
||||
next_app::{
|
||||
get_app_client_references_chunks, get_app_client_shared_chunks, get_app_page_entry,
|
||||
get_app_route_entry, AppEntry, AppPage,
|
||||
get_app_route_entry, metadata::route::get_app_metadata_route_entry, AppEntry, AppPage,
|
||||
},
|
||||
next_client::{
|
||||
get_client_module_options_context, get_client_resolve_options_context,
|
||||
|
@ -113,6 +114,11 @@ impl AppProject {
|
|||
self.app_dir
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
fn mode(&self) -> Vc<NextMode> {
|
||||
self.mode.cell()
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
fn app_entrypoints(&self) -> Vc<AppEntrypoints> {
|
||||
get_entrypoints(self.app_dir, self.project.next_config().page_extensions())
|
||||
|
@ -395,6 +401,16 @@ pub async fn app_entry_point_to_route(
|
|||
.cell(),
|
||||
),
|
||||
},
|
||||
AppEntrypoint::AppMetadata { page, metadata } => Route::AppRoute {
|
||||
endpoint: Vc::upcast(
|
||||
AppEndpoint {
|
||||
ty: AppEndpointType::Metadata { metadata },
|
||||
app_project,
|
||||
page,
|
||||
}
|
||||
.cell(),
|
||||
),
|
||||
},
|
||||
}
|
||||
.cell()
|
||||
}
|
||||
|
@ -414,6 +430,9 @@ enum AppEndpointType {
|
|||
Route {
|
||||
path: Vc<FileSystemPath>,
|
||||
},
|
||||
Metadata {
|
||||
metadata: MetadataItem,
|
||||
},
|
||||
}
|
||||
|
||||
#[turbo_tasks::value]
|
||||
|
@ -431,7 +450,6 @@ impl AppEndpoint {
|
|||
self.app_project.rsc_module_context(),
|
||||
self.app_project.edge_rsc_module_context(),
|
||||
loader_tree,
|
||||
self.app_project.app_dir(),
|
||||
self.page.clone(),
|
||||
self.app_project.project().project_path(),
|
||||
)
|
||||
|
@ -448,6 +466,18 @@ impl AppEndpoint {
|
|||
)
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn app_metadata_entry(&self, metadata: MetadataItem) -> Result<Vc<AppEntry>> {
|
||||
Ok(get_app_metadata_route_entry(
|
||||
self.app_project.rsc_module_context(),
|
||||
self.app_project.edge_rsc_module_context(),
|
||||
self.app_project.project().project_path(),
|
||||
self.page.clone(),
|
||||
*self.app_project.mode().await?,
|
||||
metadata,
|
||||
))
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
fn output_assets(self: Vc<Self>) -> Vc<OutputAssets> {
|
||||
self.output().output_assets()
|
||||
|
@ -465,6 +495,7 @@ impl AppEndpoint {
|
|||
// as we know we won't have any client references. However, for now, for simplicity's
|
||||
// sake, we just do the same thing as for pages.
|
||||
AppEndpointType::Route { path } => (self.app_route_entry(path), "route"),
|
||||
AppEndpointType::Metadata { metadata } => (self.app_metadata_entry(metadata), "route"),
|
||||
};
|
||||
|
||||
let node_root = this.app_project.project().node_root();
|
||||
|
|
|
@ -2,11 +2,11 @@ use std::collections::HashMap;
|
|||
|
||||
use anyhow::Result;
|
||||
use next_core::{
|
||||
app_structure::{find_app_dir_if_enabled, get_entrypoints, get_global_metadata, Entrypoint},
|
||||
app_structure::{find_app_dir_if_enabled, get_entrypoints, Entrypoint},
|
||||
mode::NextMode,
|
||||
next_app::{
|
||||
get_app_client_shared_chunks, get_app_page_entry, get_app_route_entry,
|
||||
get_app_route_favicon_entry, AppEntry, ClientReferencesChunks,
|
||||
metadata::route::get_app_metadata_route_entry, AppEntry, ClientReferencesChunks,
|
||||
},
|
||||
next_client::{
|
||||
get_client_module_options_context, get_client_resolve_options_context,
|
||||
|
@ -184,7 +184,7 @@ pub async fn get_app_entries(
|
|||
rsc_resolve_options_context,
|
||||
);
|
||||
|
||||
let mut entries = entrypoints
|
||||
let entries = entrypoints
|
||||
.await?
|
||||
.iter()
|
||||
.map(|(_, entrypoint)| async move {
|
||||
|
@ -194,7 +194,6 @@ pub async fn get_app_entries(
|
|||
// TODO add edge support
|
||||
rsc_context,
|
||||
*loader_tree,
|
||||
app_dir,
|
||||
page.clone(),
|
||||
project_root,
|
||||
),
|
||||
|
@ -206,24 +205,20 @@ pub async fn get_app_entries(
|
|||
page.clone(),
|
||||
project_root,
|
||||
),
|
||||
Entrypoint::AppMetadata { page, metadata } => get_app_metadata_route_entry(
|
||||
rsc_context,
|
||||
// TODO add edge support
|
||||
rsc_context,
|
||||
project_root,
|
||||
page.clone(),
|
||||
mode,
|
||||
*metadata,
|
||||
),
|
||||
})
|
||||
})
|
||||
.try_join()
|
||||
.await?;
|
||||
|
||||
let global_metadata = get_global_metadata(app_dir, next_config.page_extensions());
|
||||
let global_metadata = global_metadata.await?;
|
||||
|
||||
if let Some(favicon) = global_metadata.favicon {
|
||||
entries.push(get_app_route_favicon_entry(
|
||||
rsc_context,
|
||||
// TODO add edge support
|
||||
rsc_context,
|
||||
favicon,
|
||||
project_root,
|
||||
));
|
||||
}
|
||||
|
||||
let client_context = ModuleAssetContext::new(
|
||||
Vc::cell(Default::default()),
|
||||
client_compile_time_info,
|
||||
|
|
|
@ -14,6 +14,7 @@ async-recursion = { workspace = true }
|
|||
async-trait = { workspace = true }
|
||||
base64 = "0.21.0"
|
||||
const_format = "0.2.30"
|
||||
lazy-regex = "3.0.1"
|
||||
once_cell = { workspace = true }
|
||||
qstring = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use std::{collections::HashMap, io::Write as _, iter::once};
|
||||
use std::{collections::HashMap, io::Write as _};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use indexmap::indexmap;
|
||||
use indoc::formatdoc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
use turbo_tasks::Vc;
|
||||
use turbo_tasks::{trace::TraceRawVcs, TaskInput, Vc};
|
||||
use turbopack_binding::{
|
||||
turbo::{
|
||||
tasks::Value,
|
||||
|
@ -19,7 +20,6 @@ use turbopack_binding::{
|
|||
context::AssetContext,
|
||||
environment::ServerAddr,
|
||||
file_source::FileSource,
|
||||
issue::IssueExt,
|
||||
reference_type::{
|
||||
EcmaScriptModulesReferenceSubType, EntryReferenceSubType, ReferenceType,
|
||||
},
|
||||
|
@ -30,7 +30,6 @@ use turbopack_binding::{
|
|||
dev_server::{
|
||||
html::DevHtmlAsset,
|
||||
source::{
|
||||
asset_graph::AssetGraphContentSource,
|
||||
combined::CombinedContentSource,
|
||||
route_tree::{BaseSegment, RouteType},
|
||||
ContentSource, ContentSourceData, ContentSourceExt, NoContentSource,
|
||||
|
@ -47,7 +46,6 @@ use turbopack_binding::{
|
|||
},
|
||||
NodeEntry, NodeRenderingEntry,
|
||||
},
|
||||
r#static::fixed::FixedStaticAsset,
|
||||
turbopack::{transition::Transition, ModuleAssetContext},
|
||||
},
|
||||
};
|
||||
|
@ -55,17 +53,14 @@ use turbopack_binding::{
|
|||
use crate::{
|
||||
app_render::next_server_component_transition::NextServerComponentTransition,
|
||||
app_segment_config::{parse_segment_config_from_loader_tree, parse_segment_config_from_source},
|
||||
app_structure::{
|
||||
get_entrypoints, get_global_metadata, Entrypoint, GlobalMetadata, LoaderTree, MetadataItem,
|
||||
OptionAppDir,
|
||||
},
|
||||
app_structure::{get_entrypoints, Entrypoint, LoaderTree, MetadataItem, OptionAppDir},
|
||||
bootstrap::{route_bootstrap, BootstrapConfig},
|
||||
embed_js::{next_asset, next_js_file_path},
|
||||
env::env_for_js,
|
||||
fallback::get_fallback_page,
|
||||
loader_tree::{LoaderTreeModule, ServerComponentTransition},
|
||||
mode::NextMode,
|
||||
next_app::{AppPage, AppPath, PathSegment, UnsupportedDynamicMetadataIssue},
|
||||
next_app::{metadata::route::get_app_metadata_route_source, AppPage, AppPath, PathSegment},
|
||||
next_client::{
|
||||
context::{
|
||||
get_client_assets_path, get_client_module_options_context,
|
||||
|
@ -599,7 +594,8 @@ pub async fn create_app_source(
|
|||
return Ok(Vc::upcast(NoContentSource::new()));
|
||||
};
|
||||
let entrypoints = get_entrypoints(app_dir, next_config.page_extensions());
|
||||
let metadata = get_global_metadata(app_dir, next_config.page_extensions());
|
||||
|
||||
let mode = NextMode::DevServer;
|
||||
|
||||
let context_ssr = app_context(
|
||||
project_path,
|
||||
|
@ -610,7 +606,7 @@ pub async fn create_app_source(
|
|||
client_chunking_context,
|
||||
client_compile_time_info,
|
||||
true,
|
||||
NextMode::DevServer,
|
||||
mode,
|
||||
next_config,
|
||||
server_addr,
|
||||
output_path,
|
||||
|
@ -624,7 +620,7 @@ pub async fn create_app_source(
|
|||
client_chunking_context,
|
||||
client_compile_time_info,
|
||||
false,
|
||||
NextMode::DevServer,
|
||||
mode,
|
||||
next_config,
|
||||
server_addr,
|
||||
output_path,
|
||||
|
@ -671,6 +667,7 @@ pub async fn create_app_source(
|
|||
),
|
||||
Entrypoint::AppRoute { ref page, path } => create_app_route_source_for_route(
|
||||
page.clone(),
|
||||
mode,
|
||||
path,
|
||||
context_ssr,
|
||||
project_path,
|
||||
|
@ -681,12 +678,20 @@ pub async fn create_app_source(
|
|||
output_path,
|
||||
render_data,
|
||||
),
|
||||
Entrypoint::AppMetadata { ref page, metadata } => create_app_route_source_for_metadata(
|
||||
page.clone(),
|
||||
mode,
|
||||
context_ssr,
|
||||
project_path,
|
||||
app_dir,
|
||||
env,
|
||||
server_root,
|
||||
server_runtime_entries,
|
||||
output_path,
|
||||
render_data,
|
||||
metadata,
|
||||
),
|
||||
})
|
||||
.chain(once(create_global_metadata_source(
|
||||
app_dir,
|
||||
metadata,
|
||||
server_root,
|
||||
)))
|
||||
.collect();
|
||||
|
||||
if let Some(&Entrypoint::AppPage {
|
||||
|
@ -717,50 +722,6 @@ pub async fn create_app_source(
|
|||
Ok(Vc::upcast(CombinedContentSource { sources }.cell()))
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn create_global_metadata_source(
|
||||
app_dir: Vc<FileSystemPath>,
|
||||
metadata: Vc<GlobalMetadata>,
|
||||
server_root: Vc<FileSystemPath>,
|
||||
) -> Result<Vc<Box<dyn ContentSource>>> {
|
||||
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 = FixedStaticAsset::new(
|
||||
server_root.join(server_path.to_string()),
|
||||
Vc::upcast(FileSource::new(path)),
|
||||
);
|
||||
sources.push(Vc::upcast(AssetGraphContentSource::new_eager(
|
||||
server_root,
|
||||
Vc::upcast(asset),
|
||||
)))
|
||||
}
|
||||
MetadataItem::Dynamic { path } => {
|
||||
unsupported_metadata.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !unsupported_metadata.is_empty() {
|
||||
UnsupportedDynamicMetadataIssue {
|
||||
app_dir,
|
||||
files: unsupported_metadata,
|
||||
}
|
||||
.cell()
|
||||
.emit();
|
||||
}
|
||||
Ok(Vc::upcast(CombinedContentSource { sources }.cell()))
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn create_app_page_source_for_route(
|
||||
page: AppPage,
|
||||
|
@ -794,7 +755,6 @@ async fn create_app_page_source_for_route(
|
|||
Vc::upcast(
|
||||
AppRenderer {
|
||||
runtime_entries,
|
||||
app_dir,
|
||||
context_ssr,
|
||||
context,
|
||||
server_root,
|
||||
|
@ -839,7 +799,6 @@ async fn create_app_not_found_page_source(
|
|||
Vc::upcast(
|
||||
AppRenderer {
|
||||
runtime_entries,
|
||||
app_dir,
|
||||
context_ssr,
|
||||
context,
|
||||
server_root,
|
||||
|
@ -860,6 +819,7 @@ async fn create_app_not_found_page_source(
|
|||
#[turbo_tasks::function]
|
||||
async fn create_app_route_source_for_route(
|
||||
page: AppPage,
|
||||
mode: NextMode,
|
||||
entry_path: Vc<FileSystemPath>,
|
||||
context_ssr: Vc<ModuleAssetContext>,
|
||||
project_path: Vc<FileSystemPath>,
|
||||
|
@ -890,7 +850,58 @@ async fn create_app_route_source_for_route(
|
|||
context: context_ssr,
|
||||
runtime_entries,
|
||||
server_root,
|
||||
entry_path,
|
||||
entry: AppRouteEntry::Path(entry_path),
|
||||
mode,
|
||||
project_path,
|
||||
intermediate_output_path: intermediate_output_path_root,
|
||||
output_root: intermediate_output_path_root,
|
||||
app_dir,
|
||||
}
|
||||
.cell(),
|
||||
),
|
||||
render_data,
|
||||
should_debug("app_source"),
|
||||
);
|
||||
|
||||
Ok(source.issue_file_path(app_dir, format!("Next.js App Route {app_path}")))
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn create_app_route_source_for_metadata(
|
||||
page: AppPage,
|
||||
mode: NextMode,
|
||||
context_ssr: Vc<ModuleAssetContext>,
|
||||
project_path: Vc<FileSystemPath>,
|
||||
app_dir: Vc<FileSystemPath>,
|
||||
env: Vc<Box<dyn ProcessEnv>>,
|
||||
server_root: Vc<FileSystemPath>,
|
||||
runtime_entries: Vc<Sources>,
|
||||
intermediate_output_path_root: Vc<FileSystemPath>,
|
||||
render_data: Vc<JsonValue>,
|
||||
metadata: MetadataItem,
|
||||
) -> Result<Vc<Box<dyn ContentSource>>> {
|
||||
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) = app_path_to_segments(&app_path)?;
|
||||
|
||||
let source = create_node_api_source(
|
||||
project_path,
|
||||
env,
|
||||
base_segments,
|
||||
route_type,
|
||||
server_root,
|
||||
Vc::upcast(params_matcher),
|
||||
pathname_vc,
|
||||
Vc::upcast(
|
||||
AppRoute {
|
||||
context: context_ssr,
|
||||
runtime_entries,
|
||||
server_root,
|
||||
entry: AppRouteEntry::Metadata { metadata, page },
|
||||
mode,
|
||||
project_path,
|
||||
intermediate_output_path: intermediate_output_path_root,
|
||||
output_root: intermediate_output_path_root,
|
||||
|
@ -909,7 +920,6 @@ async fn create_app_route_source_for_route(
|
|||
#[turbo_tasks::value]
|
||||
struct AppRenderer {
|
||||
runtime_entries: Vc<Sources>,
|
||||
app_dir: Vc<FileSystemPath>,
|
||||
context_ssr: Vc<ModuleAssetContext>,
|
||||
context: Vc<ModuleAssetContext>,
|
||||
project_path: Vc<FileSystemPath>,
|
||||
|
@ -924,7 +934,6 @@ impl AppRenderer {
|
|||
async fn entry(self: Vc<Self>, with_ssr: bool) -> Result<Vc<NodeRenderingEntry>> {
|
||||
let AppRenderer {
|
||||
runtime_entries,
|
||||
app_dir,
|
||||
context_ssr,
|
||||
context,
|
||||
project_path,
|
||||
|
@ -955,15 +964,6 @@ impl AppRenderer {
|
|||
)
|
||||
.await?;
|
||||
|
||||
if !loader_tree_module.unsupported_metadata.is_empty() {
|
||||
UnsupportedDynamicMetadataIssue {
|
||||
app_dir,
|
||||
files: loader_tree_module.unsupported_metadata,
|
||||
}
|
||||
.cell()
|
||||
.emit();
|
||||
}
|
||||
|
||||
let mut result = RopeBuilder::from(
|
||||
formatdoc!(
|
||||
"
|
||||
|
@ -1078,12 +1078,22 @@ impl NodeEntry for AppRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, TaskInput, TraceRawVcs)]
|
||||
pub enum AppRouteEntry {
|
||||
Path(Vc<FileSystemPath>),
|
||||
Metadata {
|
||||
metadata: MetadataItem,
|
||||
page: AppPage,
|
||||
},
|
||||
}
|
||||
|
||||
/// The node.js renderer api routes in the app directory
|
||||
#[turbo_tasks::value]
|
||||
struct AppRoute {
|
||||
runtime_entries: Vc<Sources>,
|
||||
context: Vc<ModuleAssetContext>,
|
||||
entry_path: Vc<FileSystemPath>,
|
||||
entry: AppRouteEntry,
|
||||
mode: NextMode,
|
||||
intermediate_output_path: Vc<FileSystemPath>,
|
||||
project_path: Vc<FileSystemPath>,
|
||||
server_root: Vc<FileSystemPath>,
|
||||
|
@ -1110,13 +1120,19 @@ impl AppRoute {
|
|||
.build(),
|
||||
);
|
||||
|
||||
let entry_file_source = FileSource::new(this.entry_path);
|
||||
let entry_file_source = match this.entry {
|
||||
AppRouteEntry::Path(path) => Vc::upcast(FileSource::new(path)),
|
||||
AppRouteEntry::Metadata { metadata, ref page } => {
|
||||
get_app_metadata_route_source(page.clone(), this.mode, metadata)
|
||||
}
|
||||
};
|
||||
|
||||
let entry_asset = this.context.process(
|
||||
Vc::upcast(entry_file_source),
|
||||
entry_file_source,
|
||||
Value::new(ReferenceType::Entry(EntryReferenceSubType::AppRoute)),
|
||||
);
|
||||
|
||||
let config = parse_segment_config_from_source(entry_asset, Vc::upcast(entry_file_source));
|
||||
let config = parse_segment_config_from_source(entry_asset, entry_file_source);
|
||||
let module = match config.await?.runtime {
|
||||
Some(NextRuntime::NodeJs) | None => {
|
||||
let bootstrap_asset = next_asset("entry/app/route.ts".to_string());
|
||||
|
@ -1125,7 +1141,7 @@ impl AppRoute {
|
|||
.context
|
||||
.with_transition("next-route".to_string())
|
||||
.process(
|
||||
Vc::upcast(entry_file_source),
|
||||
entry_file_source,
|
||||
Value::new(ReferenceType::Entry(EntryReferenceSubType::AppRoute)),
|
||||
);
|
||||
|
||||
|
@ -1144,7 +1160,7 @@ impl AppRoute {
|
|||
.context
|
||||
.with_transition("next-edge-route".to_string())
|
||||
.process(
|
||||
Vc::upcast(entry_file_source),
|
||||
entry_file_source,
|
||||
Value::new(ReferenceType::Entry(EntryReferenceSubType::AppRoute)),
|
||||
);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use indexmap::{
|
||||
|
@ -6,8 +6,6 @@ use indexmap::{
|
|||
map::{Entry, OccupiedEntry},
|
||||
IndexMap,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use turbo_tasks::{
|
||||
debug::ValueDebugFormat, trace::TraceRawVcs, Completion, Completions, TaskInput, ValueToString,
|
||||
|
@ -19,7 +17,13 @@ use turbopack_binding::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
next_app::{AppPage, AppPath},
|
||||
next_app::{
|
||||
metadata::{
|
||||
match_global_metadata_file, match_local_metadata_file, normalize_metadata_route,
|
||||
GlobalMetadataFileMatch, MetadataFileMatch,
|
||||
},
|
||||
AppPage, AppPath, PageType,
|
||||
},
|
||||
next_config::NextConfig,
|
||||
next_import_map::get_next_package,
|
||||
};
|
||||
|
@ -108,6 +112,49 @@ pub enum MetadataItem {
|
|||
Dynamic { path: Vc<FileSystemPath> },
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
pub async fn get_metadata_route_name(meta: MetadataItem) -> Result<Vc<String>> {
|
||||
Ok(match meta {
|
||||
MetadataItem::Static { path } => {
|
||||
let path_value = path.await?;
|
||||
Vc::cell(path_value.file_name().to_string())
|
||||
}
|
||||
MetadataItem::Dynamic { path } => {
|
||||
let Some(stem) = &*path.file_stem().await? else {
|
||||
bail!(
|
||||
"unable to resolve file stem for metadata item at {}",
|
||||
path.to_string().await?
|
||||
);
|
||||
};
|
||||
|
||||
match stem.as_str() {
|
||||
"robots" => Vc::cell("robots.txt".to_string()),
|
||||
"manifest" => Vc::cell("manifest.webmanifest".to_string()),
|
||||
"sitemap" => Vc::cell("sitemap.xml".to_string()),
|
||||
_ => Vc::cell(stem.clone()),
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
impl MetadataItem {
|
||||
pub fn into_path(self) -> Vc<FileSystemPath> {
|
||||
match self {
|
||||
MetadataItem::Static { path } => path,
|
||||
MetadataItem::Dynamic { path } => path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MetadataWithAltItem> for MetadataItem {
|
||||
fn from(value: MetadataWithAltItem) -> Self {
|
||||
match value {
|
||||
MetadataWithAltItem::Static { path, .. } => MetadataItem::Static { path },
|
||||
MetadataWithAltItem::Dynamic { path } => MetadataItem::Dynamic { path },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
@ -119,10 +166,8 @@ pub struct Metadata {
|
|||
pub twitter: Vec<MetadataWithAltItem>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub open_graph: Vec<MetadataWithAltItem>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub favicon: Vec<MetadataWithAltItem>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub manifest: Option<MetadataItem>,
|
||||
pub sitemap: Option<MetadataItem>,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
|
@ -132,15 +177,13 @@ impl Metadata {
|
|||
apple,
|
||||
twitter,
|
||||
open_graph,
|
||||
favicon,
|
||||
manifest,
|
||||
sitemap,
|
||||
} = self;
|
||||
icon.is_empty()
|
||||
&& apple.is_empty()
|
||||
&& twitter.is_empty()
|
||||
&& open_graph.is_empty()
|
||||
&& favicon.is_empty()
|
||||
&& manifest.is_none()
|
||||
&& sitemap.is_none()
|
||||
}
|
||||
|
||||
fn merge(a: &Self, b: &Self) -> Self {
|
||||
|
@ -154,8 +197,7 @@ impl Metadata {
|
|||
.chain(b.open_graph.iter())
|
||||
.copied()
|
||||
.collect(),
|
||||
favicon: a.favicon.iter().chain(b.favicon.iter()).copied().collect(),
|
||||
manifest: a.manifest.or(b.manifest),
|
||||
sitemap: a.sitemap.or(b.sitemap),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -169,7 +211,7 @@ pub struct GlobalMetadata {
|
|||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub robots: Option<MetadataItem>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sitemap: Option<MetadataItem>,
|
||||
pub manifest: Option<MetadataItem>,
|
||||
}
|
||||
|
||||
impl GlobalMetadata {
|
||||
|
@ -177,9 +219,9 @@ impl GlobalMetadata {
|
|||
let GlobalMetadata {
|
||||
favicon,
|
||||
robots,
|
||||
sitemap,
|
||||
manifest,
|
||||
} = self;
|
||||
favicon.is_none() && robots.is_none() && sitemap.is_none()
|
||||
favicon.is_none() && robots.is_none() && manifest.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,46 +297,6 @@ pub async fn find_app_dir_if_enabled(project_path: Vc<FileSystemPath>) -> Result
|
|||
Ok(find_app_dir(project_path))
|
||||
}
|
||||
|
||||
static STATIC_LOCAL_METADATA: Lazy<HashMap<&'static str, &'static [&'static str]>> =
|
||||
Lazy::new(|| {
|
||||
HashMap::from([
|
||||
(
|
||||
"icon",
|
||||
&["ico", "jpg", "jpeg", "png", "svg"] as &'static [&'static str],
|
||||
),
|
||||
("apple-icon", &["jpg", "jpeg", "png"]),
|
||||
("opengraph-image", &["jpg", "jpeg", "png", "gif"]),
|
||||
("twitter-image", &["jpg", "jpeg", "png", "gif"]),
|
||||
("favicon", &["ico"]),
|
||||
("manifest", &["webmanifest", "json"]),
|
||||
])
|
||||
});
|
||||
|
||||
static STATIC_GLOBAL_METADATA: Lazy<HashMap<&'static str, &'static [&'static str]>> =
|
||||
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<Regex> = 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]
|
||||
async fn get_directory_tree(
|
||||
dir: Vc<FileSystemPath>,
|
||||
|
@ -312,7 +314,6 @@ async fn get_directory_tree(
|
|||
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 {
|
||||
|
@ -328,59 +329,58 @@ async fn get_directory_tree(
|
|||
"not-found" => components.not_found = 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, 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 });
|
||||
let Some(MetadataFileMatch {
|
||||
metadata_type,
|
||||
number,
|
||||
dynamic,
|
||||
}) = match_local_metadata_file(basename.as_str(), &page_extensions_value)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let entry = match metadata_type {
|
||||
"icon" => &mut metadata_icon,
|
||||
"apple-icon" => &mut metadata_apple,
|
||||
"twitter-image" => &mut metadata_twitter,
|
||||
"opengraph-image" => &mut metadata_open_graph,
|
||||
"sitemap" => {
|
||||
if dynamic {
|
||||
components.metadata.sitemap =
|
||||
Some(MetadataItem::Dynamic { path: file });
|
||||
} else {
|
||||
components.metadata.sitemap = Some(MetadataItem::Static { path: file });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
_ => 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),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(entry) = entry {
|
||||
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,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
if dynamic {
|
||||
entry.push((number, MetadataWithAltItem::Dynamic { path: file }));
|
||||
continue;
|
||||
}
|
||||
|
||||
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((
|
||||
number,
|
||||
MetadataWithAltItem::Static {
|
||||
path: file,
|
||||
alt_path,
|
||||
},
|
||||
));
|
||||
}
|
||||
DirectoryEntry::Directory(dir) => {
|
||||
// appDir ignores paths starting with an underscore
|
||||
|
@ -394,7 +394,7 @@ async fn get_directory_tree(
|
|||
}
|
||||
}
|
||||
|
||||
fn sort<T>(mut list: Vec<(i32, T)>) -> Vec<T> {
|
||||
fn sort<T>(mut list: Vec<(Option<u32>, T)>) -> Vec<T> {
|
||||
list.sort_by_key(|(num, _)| *num);
|
||||
list.into_iter().map(|(_, item)| item).collect()
|
||||
}
|
||||
|
@ -403,7 +403,6 @@ async fn get_directory_tree(
|
|||
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,
|
||||
|
@ -415,9 +414,11 @@ async fn get_directory_tree(
|
|||
#[turbo_tasks::value]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoaderTree {
|
||||
pub page: AppPage,
|
||||
pub segment: String,
|
||||
pub parallel_routes: IndexMap<String, Vc<LoaderTree>>,
|
||||
pub components: Vc<Components>,
|
||||
pub global_metadata: Vc<GlobalMetadata>,
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
|
@ -443,9 +444,12 @@ async fn merge_loader_trees(
|
|||
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())
|
||||
}
|
||||
|
@ -462,6 +466,10 @@ pub enum Entrypoint {
|
|||
page: AppPage,
|
||||
path: Vc<FileSystemPath>,
|
||||
},
|
||||
AppMetadata {
|
||||
page: AppPage,
|
||||
metadata: MetadataItem,
|
||||
},
|
||||
}
|
||||
|
||||
#[turbo_tasks::value(transparent)]
|
||||
|
@ -568,6 +576,12 @@ async fn add_app_page(
|
|||
} => {
|
||||
conflict("route", existing_page);
|
||||
}
|
||||
Entrypoint::AppMetadata {
|
||||
page: existing_page,
|
||||
..
|
||||
} => {
|
||||
conflict("metadata", existing_page);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -607,6 +621,55 @@ fn add_app_route(
|
|||
} => {
|
||||
conflict("route", existing_page);
|
||||
}
|
||||
Entrypoint::AppMetadata {
|
||||
page: existing_page,
|
||||
..
|
||||
} => {
|
||||
conflict("metadata", existing_page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_app_metadata_route(
|
||||
app_dir: Vc<FileSystemPath>,
|
||||
result: &mut IndexMap<String, Entrypoint>,
|
||||
page: AppPage,
|
||||
metadata: MetadataItem,
|
||||
) {
|
||||
let pathname = AppPath::from(page.clone());
|
||||
|
||||
let e = match result.entry(format!("{pathname}")) {
|
||||
Entry::Occupied(e) => e,
|
||||
Entry::Vacant(e) => {
|
||||
e.insert(Entrypoint::AppMetadata { page, metadata });
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let conflict = |existing_name: &str, existing_page: &AppPage| {
|
||||
conflict_issue(app_dir, &e, "metadata", 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);
|
||||
}
|
||||
Entrypoint::AppMetadata {
|
||||
page: existing_page,
|
||||
..
|
||||
} => {
|
||||
conflict("metadata", existing_page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -615,20 +678,32 @@ pub fn get_entrypoints(
|
|||
app_dir: Vc<FileSystemPath>,
|
||||
page_extensions: Vc<Vec<String>>,
|
||||
) -> Vc<Entrypoints> {
|
||||
directory_tree_to_entrypoints(app_dir, get_directory_tree(app_dir, page_extensions))
|
||||
directory_tree_to_entrypoints(
|
||||
app_dir,
|
||||
get_directory_tree(app_dir, page_extensions),
|
||||
get_global_metadata(app_dir, page_extensions),
|
||||
)
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
fn directory_tree_to_entrypoints(
|
||||
app_dir: Vc<FileSystemPath>,
|
||||
directory_tree: Vc<DirectoryTree>,
|
||||
global_metadata: Vc<GlobalMetadata>,
|
||||
) -> Vc<Entrypoints> {
|
||||
directory_tree_to_entrypoints_internal(app_dir, "".to_string(), directory_tree, AppPage::new())
|
||||
directory_tree_to_entrypoints_internal(
|
||||
app_dir,
|
||||
global_metadata,
|
||||
"".to_string(),
|
||||
directory_tree,
|
||||
AppPage::new(),
|
||||
)
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn directory_tree_to_entrypoints_internal(
|
||||
app_dir: Vc<FileSystemPath>,
|
||||
global_metadata: Vc<GlobalMetadata>,
|
||||
directory_name: String,
|
||||
directory_tree: Vc<DirectoryTree>,
|
||||
app_page: AppPage,
|
||||
|
@ -646,9 +721,10 @@ async fn directory_tree_to_entrypoints_internal(
|
|||
add_app_page(
|
||||
app_dir,
|
||||
&mut result,
|
||||
app_page.clone(),
|
||||
app_page.clone().complete(PageType::Page)?,
|
||||
if current_level_is_parallel_route {
|
||||
LoaderTree {
|
||||
page: app_page.clone(),
|
||||
segment: "__PAGE__".to_string(),
|
||||
parallel_routes: IndexMap::new(),
|
||||
components: Components {
|
||||
|
@ -656,13 +732,16 @@ async fn directory_tree_to_entrypoints_internal(
|
|||
..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 {
|
||||
|
@ -670,10 +749,12 @@ async fn directory_tree_to_entrypoints_internal(
|
|||
..Default::default()
|
||||
}
|
||||
.cell(),
|
||||
global_metadata,
|
||||
}
|
||||
.cell(),
|
||||
},
|
||||
components: components.without_leafs().cell(),
|
||||
global_metadata,
|
||||
}
|
||||
.cell()
|
||||
},
|
||||
|
@ -685,9 +766,10 @@ async fn directory_tree_to_entrypoints_internal(
|
|||
add_app_page(
|
||||
app_dir,
|
||||
&mut result,
|
||||
app_page.clone(),
|
||||
app_page.clone().complete(PageType::Page)?,
|
||||
if current_level_is_parallel_route {
|
||||
LoaderTree {
|
||||
page: app_page.clone(),
|
||||
segment: "__DEFAULT__".to_string(),
|
||||
parallel_routes: IndexMap::new(),
|
||||
components: Components {
|
||||
|
@ -695,24 +777,29 @@ async fn directory_tree_to_entrypoints_internal(
|
|||
..Default::default()
|
||||
}
|
||||
.cell(),
|
||||
global_metadata,
|
||||
}
|
||||
.cell()
|
||||
} else {
|
||||
LoaderTree {
|
||||
page: app_page.clone(),
|
||||
segment: directory_name.to_string(),
|
||||
parallel_routes: indexmap! {
|
||||
"children".to_string() => LoaderTree {
|
||||
segment: "__DEFAULT__".to_string(),
|
||||
parallel_routes: IndexMap::new(),
|
||||
components: Components {
|
||||
default: Some(default),
|
||||
..Default::default()
|
||||
}
|
||||
.cell(),
|
||||
"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()
|
||||
},
|
||||
|
@ -721,18 +808,67 @@ async fn directory_tree_to_entrypoints_internal(
|
|||
}
|
||||
|
||||
if let Some(route) = components.route {
|
||||
add_app_route(app_dir, &mut result, app_page.clone(), route);
|
||||
add_app_route(
|
||||
app_dir,
|
||||
&mut result,
|
||||
app_page.clone().complete(PageType::Route)?,
|
||||
route,
|
||||
);
|
||||
}
|
||||
|
||||
let Metadata {
|
||||
icon,
|
||||
apple,
|
||||
twitter,
|
||||
open_graph,
|
||||
sitemap,
|
||||
} = &components.metadata;
|
||||
|
||||
for meta in sitemap
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(icon.iter().copied().map(MetadataItem::from))
|
||||
.chain(apple.iter().copied().map(MetadataItem::from))
|
||||
.chain(twitter.iter().copied().map(MetadataItem::from))
|
||||
.chain(open_graph.iter().copied().map(MetadataItem::from))
|
||||
{
|
||||
let app_page = app_page.clone_push_str(&get_metadata_route_name(meta).await?)?;
|
||||
|
||||
add_app_metadata_route(
|
||||
app_dir,
|
||||
&mut result,
|
||||
normalize_metadata_route(app_page)?,
|
||||
meta,
|
||||
);
|
||||
}
|
||||
|
||||
// root path: /
|
||||
if app_page.len() == 0 {
|
||||
if app_page.is_root() {
|
||||
let GlobalMetadata {
|
||||
favicon,
|
||||
robots,
|
||||
manifest,
|
||||
} = &*global_metadata.await?;
|
||||
|
||||
for meta in favicon.iter().chain(robots.iter()).chain(manifest.iter()) {
|
||||
let app_page = app_page.clone_push_str(&get_metadata_route_name(*meta).await?)?;
|
||||
|
||||
add_app_metadata_route(
|
||||
app_dir,
|
||||
&mut result,
|
||||
normalize_metadata_route(app_page)?,
|
||||
*meta,
|
||||
);
|
||||
}
|
||||
// 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 {
|
||||
let dev_not_found_tree = 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 {
|
||||
|
@ -740,10 +876,12 @@ async fn directory_tree_to_entrypoints_internal(
|
|||
..Default::default()
|
||||
}
|
||||
.cell(),
|
||||
global_metadata,
|
||||
}
|
||||
.cell(),
|
||||
},
|
||||
components: components.without_leafs().cell(),
|
||||
global_metadata,
|
||||
}
|
||||
.cell();
|
||||
|
||||
|
@ -759,9 +897,11 @@ async fn directory_tree_to_entrypoints_internal(
|
|||
// Create default not-found page for production if there's no customized
|
||||
// not-found
|
||||
let prod_not_found_tree = 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 {
|
||||
|
@ -769,10 +909,12 @@ async fn directory_tree_to_entrypoints_internal(
|
|||
..Default::default()
|
||||
}
|
||||
.cell(),
|
||||
global_metadata,
|
||||
}
|
||||
.cell(),
|
||||
},
|
||||
components: components.without_leafs().cell(),
|
||||
global_metadata,
|
||||
}
|
||||
.cell();
|
||||
|
||||
|
@ -791,9 +933,10 @@ async fn directory_tree_to_entrypoints_internal(
|
|||
|
||||
let map = directory_tree_to_entrypoints_internal(
|
||||
app_dir,
|
||||
global_metadata,
|
||||
subdir_name.to_string(),
|
||||
subdirectory,
|
||||
app_page,
|
||||
app_page.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
@ -808,11 +951,13 @@ async fn directory_tree_to_entrypoints_internal(
|
|||
} 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?;
|
||||
|
@ -821,6 +966,9 @@ async fn directory_tree_to_entrypoints_internal(
|
|||
Entrypoint::AppRoute { ref page, path } => {
|
||||
add_app_route(app_dir, &mut result, page.clone(), path);
|
||||
}
|
||||
Entrypoint::AppMetadata { ref page, metadata } => {
|
||||
add_app_metadata_route(app_dir, &mut result, page.clone(), metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -845,23 +993,29 @@ pub async fn get_global_metadata(
|
|||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
let DirectoryEntry::File(file) = *entry else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(GlobalMetadataFileMatch {
|
||||
metadata_type,
|
||||
dynamic,
|
||||
}) = match_global_metadata_file(basename, &page_extensions.await?)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let entry = match metadata_type {
|
||||
"favicon" => &mut metadata.favicon,
|
||||
"manifest" => &mut metadata.manifest,
|
||||
"robots" => &mut metadata.robots,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if dynamic {
|
||||
*entry = Some(MetadataItem::Dynamic { path: file });
|
||||
} else {
|
||||
*entry = Some(MetadataItem::Static { path: file });
|
||||
}
|
||||
// TODO(WEB-952) handle symlinks in app dir
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_recursion::async_recursion;
|
||||
use indexmap::IndexMap;
|
||||
|
@ -12,13 +14,16 @@ use turbopack_binding::turbopack::{
|
|||
reference_type::{EcmaScriptModulesReferenceSubType, InnerAssets, ReferenceType},
|
||||
},
|
||||
ecmascript::{magic_identifier, text::TextContentFileSource, utils::StringifyJs},
|
||||
r#static::StaticModuleAsset,
|
||||
turbopack::{transition::Transition, ModuleAssetContext},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_structure::{Components, LoaderTree, Metadata, MetadataItem, MetadataWithAltItem},
|
||||
app_structure::{
|
||||
get_metadata_route_name, Components, GlobalMetadata, LoaderTree, Metadata, MetadataItem,
|
||||
MetadataWithAltItem,
|
||||
},
|
||||
mode::NextMode,
|
||||
next_app::{metadata::image::dynamic_image_metadata_source, AppPage},
|
||||
next_image::module::{BlurPlaceholderMode, StructuredImageModuleType},
|
||||
};
|
||||
|
||||
|
@ -28,7 +33,6 @@ pub struct LoaderTreeBuilder {
|
|||
imports: Vec<String>,
|
||||
loader_tree_code: String,
|
||||
context: Vc<ModuleAssetContext>,
|
||||
unsupported_metadata: Vec<Vc<FileSystemPath>>,
|
||||
mode: NextMode,
|
||||
server_component_transition: ServerComponentTransition,
|
||||
pages: Vec<Vc<FileSystemPath>>,
|
||||
|
@ -77,7 +81,6 @@ impl LoaderTreeBuilder {
|
|||
imports: Vec::new(),
|
||||
loader_tree_code: String::new(),
|
||||
context,
|
||||
unsupported_metadata: Vec::new(),
|
||||
server_component_transition,
|
||||
mode,
|
||||
pages: Vec::new(),
|
||||
|
@ -95,8 +98,6 @@ impl LoaderTreeBuilder {
|
|||
ty: ComponentType,
|
||||
component: Option<Vc<FileSystemPath>>,
|
||||
) -> Result<()> {
|
||||
use std::fmt::Write;
|
||||
|
||||
if let Some(component) = component {
|
||||
if matches!(ty, ComponentType::Page) {
|
||||
self.pages.push(component);
|
||||
|
@ -164,7 +165,12 @@ impl LoaderTreeBuilder {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn write_metadata(&mut self, metadata: &Metadata) -> Result<()> {
|
||||
async fn write_metadata(
|
||||
&mut self,
|
||||
app_page: &AppPage,
|
||||
metadata: &Metadata,
|
||||
global_metadata: &GlobalMetadata,
|
||||
) -> Result<()> {
|
||||
if metadata.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
@ -173,118 +179,176 @@ impl LoaderTreeBuilder {
|
|||
apple,
|
||||
twitter,
|
||||
open_graph,
|
||||
favicon,
|
||||
manifest,
|
||||
sitemap: _,
|
||||
} = metadata;
|
||||
let GlobalMetadata {
|
||||
favicon: _,
|
||||
manifest,
|
||||
robots: _,
|
||||
} = global_metadata;
|
||||
|
||||
self.loader_tree_code += " metadata: {";
|
||||
self.write_metadata_items("icon", favicon.iter().chain(icon.iter()))?;
|
||||
self.write_metadata_items("apple", apple.iter())?;
|
||||
self.write_metadata_items("twitter", twitter.iter())?;
|
||||
self.write_metadata_items("openGraph", open_graph.iter())?;
|
||||
self.write_metadata_manifest(*manifest)?;
|
||||
self.write_metadata_items(app_page, "icon", icon.iter())
|
||||
.await?;
|
||||
self.write_metadata_items(app_page, "apple", apple.iter())
|
||||
.await?;
|
||||
self.write_metadata_items(app_page, "twitter", twitter.iter())
|
||||
.await?;
|
||||
self.write_metadata_items(app_page, "openGraph", open_graph.iter())
|
||||
.await?;
|
||||
self.write_metadata_manifest(*manifest).await?;
|
||||
self.loader_tree_code += " },";
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_metadata_manifest(&mut self, manifest: Option<MetadataItem>) -> Result<()> {
|
||||
async fn write_metadata_manifest(&mut self, manifest: Option<MetadataItem>) -> Result<()> {
|
||||
let Some(manifest) = manifest else {
|
||||
return Ok(());
|
||||
};
|
||||
match manifest {
|
||||
MetadataItem::Static { path } => {
|
||||
use std::fmt::Write;
|
||||
let i = self.unique_number();
|
||||
let identifier = magic_identifier::mangle(&format!("manifest #{i}"));
|
||||
let inner_module_id = format!("METADATA_{i}");
|
||||
self.imports
|
||||
.push(format!("import {identifier} from \"{inner_module_id}\";"));
|
||||
self.inner_assets.insert(
|
||||
inner_module_id,
|
||||
Vc::upcast(StaticModuleAsset::new(
|
||||
Vc::upcast(FileSource::new(path)),
|
||||
Vc::upcast(self.context),
|
||||
)),
|
||||
);
|
||||
writeln!(self.loader_tree_code, " manifest: {identifier},")?;
|
||||
}
|
||||
MetadataItem::Dynamic { path } => {
|
||||
self.unsupported_metadata.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
let manifest_route = &format!("/{}", get_metadata_route_name(manifest).await?);
|
||||
writeln!(
|
||||
self.loader_tree_code,
|
||||
" manifest: {},",
|
||||
StringifyJs(manifest_route)
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_metadata_items<'a>(
|
||||
async fn write_metadata_items<'a>(
|
||||
&mut self,
|
||||
app_page: &AppPage,
|
||||
name: &str,
|
||||
it: impl Iterator<Item = &'a MetadataWithAltItem>,
|
||||
) -> Result<()> {
|
||||
use std::fmt::Write;
|
||||
let mut it = it.peekable();
|
||||
if it.peek().is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
writeln!(self.loader_tree_code, " {name}: [")?;
|
||||
for item in it {
|
||||
self.write_metadata_item(name, item)?;
|
||||
self.write_metadata_item(app_page, name, item).await?;
|
||||
}
|
||||
writeln!(self.loader_tree_code, " ],")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_metadata_item(&mut self, name: &str, item: &MetadataWithAltItem) -> Result<()> {
|
||||
use std::fmt::Write;
|
||||
async fn write_metadata_item(
|
||||
&mut self,
|
||||
app_page: &AppPage,
|
||||
name: &str,
|
||||
item: &MetadataWithAltItem,
|
||||
) -> Result<()> {
|
||||
match item {
|
||||
MetadataWithAltItem::Static { path, alt_path } => {
|
||||
self.write_static_metadata_item(app_page, name, item, *path, *alt_path)
|
||||
.await?;
|
||||
}
|
||||
MetadataWithAltItem::Dynamic { path, .. } => {
|
||||
let i = self.unique_number();
|
||||
let identifier = magic_identifier::mangle(&format!("{name} #{i}"));
|
||||
let inner_module_id = format!("METADATA_{i}");
|
||||
|
||||
self.imports
|
||||
.push(format!("import {identifier} from \"{inner_module_id}\";"));
|
||||
|
||||
let source = dynamic_image_metadata_source(
|
||||
Vc::upcast(self.context),
|
||||
*path,
|
||||
name.to_string(),
|
||||
app_page.clone(),
|
||||
);
|
||||
|
||||
self.inner_assets.insert(
|
||||
inner_module_id,
|
||||
self.context.process(
|
||||
source,
|
||||
Value::new(ReferenceType::EcmaScriptModules(
|
||||
EcmaScriptModulesReferenceSubType::Undefined,
|
||||
)),
|
||||
),
|
||||
);
|
||||
|
||||
let s = " ";
|
||||
writeln!(self.loader_tree_code, "{s}{identifier},")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_static_metadata_item(
|
||||
&mut self,
|
||||
app_page: &AppPage,
|
||||
name: &str,
|
||||
item: &MetadataWithAltItem,
|
||||
path: Vc<FileSystemPath>,
|
||||
alt_path: Option<Vc<FileSystemPath>>,
|
||||
) -> Result<()> {
|
||||
let i = self.unique_number();
|
||||
let identifier = magic_identifier::mangle(&format!("{name} #{i}"));
|
||||
let inner_module_id = format!("METADATA_{i}");
|
||||
let helper_import = "import { fillMetadataSegment } from \
|
||||
\"next/dist/lib/metadata/get-metadata-route\""
|
||||
.to_string();
|
||||
if !self.imports.contains(&helper_import) {
|
||||
self.imports.push(helper_import);
|
||||
}
|
||||
|
||||
self.imports
|
||||
.push(format!("import {identifier} from \"{inner_module_id}\";"));
|
||||
self.inner_assets.insert(
|
||||
inner_module_id,
|
||||
Vc::upcast(StructuredImageModuleType::create_module(
|
||||
Vc::upcast(FileSource::new(path)),
|
||||
BlurPlaceholderMode::None,
|
||||
self.context,
|
||||
)),
|
||||
);
|
||||
|
||||
let s = " ";
|
||||
match item {
|
||||
MetadataWithAltItem::Static { path, alt_path } => {
|
||||
self.inner_assets.insert(
|
||||
inner_module_id,
|
||||
Vc::upcast(StructuredImageModuleType::create_module(
|
||||
Vc::upcast(FileSource::new(*path)),
|
||||
BlurPlaceholderMode::None,
|
||||
self.context,
|
||||
)),
|
||||
);
|
||||
writeln!(self.loader_tree_code, "{s}(async (props) => [{{")?;
|
||||
writeln!(self.loader_tree_code, "{s} url: {identifier}.src,")?;
|
||||
let numeric_sizes = name == "twitter" || name == "openGraph";
|
||||
if numeric_sizes {
|
||||
writeln!(self.loader_tree_code, "{s} width: {identifier}.width,")?;
|
||||
writeln!(self.loader_tree_code, "{s} height: {identifier}.height,")?;
|
||||
} else {
|
||||
writeln!(
|
||||
self.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}");
|
||||
self.imports
|
||||
.push(format!("import {identifier} from \"{inner_module_id}\";"));
|
||||
self.inner_assets.insert(
|
||||
inner_module_id,
|
||||
self.context.process(
|
||||
Vc::upcast(TextContentFileSource::new(Vc::upcast(FileSource::new(
|
||||
*alt_path,
|
||||
)))),
|
||||
Value::new(ReferenceType::Internal(InnerAssets::empty())),
|
||||
),
|
||||
);
|
||||
writeln!(self.loader_tree_code, "{s} alt: {identifier},")?;
|
||||
}
|
||||
writeln!(self.loader_tree_code, "{s}}}]),")?;
|
||||
}
|
||||
MetadataWithAltItem::Dynamic { path, .. } => {
|
||||
self.unsupported_metadata.push(*path);
|
||||
}
|
||||
writeln!(self.loader_tree_code, "{s}(async (props) => [{{")?;
|
||||
|
||||
let metadata_route = &*get_metadata_route_name((*item).into()).await?;
|
||||
writeln!(
|
||||
self.loader_tree_code,
|
||||
"{s} url: fillMetadataSegment({}, props.params, {}) + \
|
||||
`?${{{identifier}.src.split(\"/\").splice(-1)[0]}}`,",
|
||||
StringifyJs(&app_page.to_string()),
|
||||
StringifyJs(metadata_route),
|
||||
)?;
|
||||
|
||||
let numeric_sizes = name == "twitter" || name == "openGraph";
|
||||
if numeric_sizes {
|
||||
writeln!(self.loader_tree_code, "{s} width: {identifier}.width,")?;
|
||||
writeln!(self.loader_tree_code, "{s} height: {identifier}.height,")?;
|
||||
} else {
|
||||
writeln!(
|
||||
self.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}");
|
||||
self.imports
|
||||
.push(format!("import {identifier} from \"{inner_module_id}\";"));
|
||||
self.inner_assets.insert(
|
||||
inner_module_id,
|
||||
self.context.process(
|
||||
Vc::upcast(TextContentFileSource::new(Vc::upcast(FileSource::new(
|
||||
alt_path,
|
||||
)))),
|
||||
Value::new(ReferenceType::Internal(InnerAssets::empty())),
|
||||
),
|
||||
);
|
||||
|
||||
writeln!(self.loader_tree_code, "{s} alt: {identifier},")?;
|
||||
}
|
||||
|
||||
writeln!(self.loader_tree_code, "{s}}}]),")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -293,9 +357,11 @@ impl LoaderTreeBuilder {
|
|||
use std::fmt::Write;
|
||||
|
||||
let LoaderTree {
|
||||
page: app_page,
|
||||
segment,
|
||||
parallel_routes,
|
||||
components,
|
||||
global_metadata,
|
||||
} = &*loader_tree.await?;
|
||||
|
||||
writeln!(
|
||||
|
@ -333,7 +399,8 @@ impl LoaderTreeBuilder {
|
|||
.await?;
|
||||
self.write_component(ComponentType::NotFound, *not_found)
|
||||
.await?;
|
||||
self.write_metadata(metadata)?;
|
||||
self.write_metadata(app_page, metadata, &*global_metadata.await?)
|
||||
.await?;
|
||||
write!(self.loader_tree_code, "}}]")?;
|
||||
Ok(())
|
||||
}
|
||||
|
@ -344,7 +411,6 @@ impl LoaderTreeBuilder {
|
|||
imports: self.imports,
|
||||
loader_tree_code: self.loader_tree_code,
|
||||
inner_assets: self.inner_assets,
|
||||
unsupported_metadata: self.unsupported_metadata,
|
||||
pages: self.pages,
|
||||
})
|
||||
}
|
||||
|
@ -354,7 +420,6 @@ pub struct LoaderTreeModule {
|
|||
pub imports: Vec<String>,
|
||||
pub loader_tree_code: String,
|
||||
pub inner_assets: IndexMap<String, Vc<Box<dyn Module>>>,
|
||||
pub unsupported_metadata: Vec<Vc<FileSystemPath>>,
|
||||
pub pages: Vec<Vc<FileSystemPath>>,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,22 +1,8 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use turbo_tasks::{debug::ValueDebugFormat, trace::TraceRawVcs, TaskInput};
|
||||
use turbo_tasks::TaskInput;
|
||||
|
||||
/// The mode in which Next.js is running.
|
||||
#[derive(
|
||||
Debug,
|
||||
Copy,
|
||||
Clone,
|
||||
TaskInput,
|
||||
Eq,
|
||||
PartialEq,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
Hash,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
TraceRawVcs,
|
||||
ValueDebugFormat,
|
||||
)]
|
||||
#[turbo_tasks::value(shared)]
|
||||
#[derive(Debug, Copy, Clone, TaskInput, Ord, PartialOrd, Hash)]
|
||||
pub enum NextMode {
|
||||
/// `next dev --turbo`
|
||||
DevServer,
|
||||
|
|
|
@ -6,8 +6,8 @@ use turbopack_binding::{
|
|||
turbo::tasks_fs::{rope::RopeBuilder, File, FileSystemPath},
|
||||
turbopack::{
|
||||
core::{
|
||||
asset::AssetContent, context::AssetContext, issue::IssueExt,
|
||||
reference_type::ReferenceType, virtual_source::VirtualSource,
|
||||
asset::AssetContent, context::AssetContext, reference_type::ReferenceType,
|
||||
virtual_source::VirtualSource,
|
||||
},
|
||||
ecmascript::{chunk::EcmascriptChunkPlaceable, utils::StringifyJs},
|
||||
turbopack::ModuleAssetContext,
|
||||
|
@ -19,7 +19,7 @@ use crate::{
|
|||
app_structure::LoaderTree,
|
||||
loader_tree::{LoaderTreeModule, ServerComponentTransition},
|
||||
mode::NextMode,
|
||||
next_app::{AppPage, AppPath, UnsupportedDynamicMetadataIssue},
|
||||
next_app::{AppPage, AppPath},
|
||||
next_server_component::NextServerComponentTransition,
|
||||
parse_segment_config_from_loader_tree,
|
||||
util::{load_next_js_template, virtual_next_js_template_path, NextRuntime},
|
||||
|
@ -31,7 +31,6 @@ pub async fn get_app_page_entry(
|
|||
nodejs_context: Vc<ModuleAssetContext>,
|
||||
edge_context: Vc<ModuleAssetContext>,
|
||||
loader_tree: Vc<LoaderTree>,
|
||||
app_dir: Vc<FileSystemPath>,
|
||||
page: AppPage,
|
||||
project_root: Vc<FileSystemPath>,
|
||||
) -> Result<Vc<AppEntry>> {
|
||||
|
@ -57,19 +56,9 @@ pub async fn get_app_page_entry(
|
|||
inner_assets,
|
||||
imports,
|
||||
loader_tree_code,
|
||||
unsupported_metadata,
|
||||
pages,
|
||||
} = loader_tree;
|
||||
|
||||
if !unsupported_metadata.is_empty() {
|
||||
UnsupportedDynamicMetadataIssue {
|
||||
app_dir,
|
||||
files: unsupported_metadata,
|
||||
}
|
||||
.cell()
|
||||
.emit();
|
||||
}
|
||||
|
||||
let mut result = RopeBuilder::default();
|
||||
|
||||
for import in imports {
|
||||
|
@ -81,8 +70,6 @@ pub async fn get_app_page_entry(
|
|||
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";
|
||||
|
||||
// Load the file from the next.js codebase.
|
||||
|
@ -100,7 +87,7 @@ pub async fn get_app_page_entry(
|
|||
)
|
||||
.replace(
|
||||
"\"VAR_ORIGINAL_PATHNAME\"",
|
||||
&StringifyJs(&original_page_name).to_string(),
|
||||
&StringifyJs(&original_name).to_string(),
|
||||
)
|
||||
// TODO(alexkirsz) Support custom global error.
|
||||
.replace(
|
||||
|
@ -154,20 +141,9 @@ pub async fn get_app_page_entry(
|
|||
|
||||
Ok(AppEntry {
|
||||
pathname: pathname.to_string(),
|
||||
original_name: original_page_name,
|
||||
original_name,
|
||||
rsc_entry,
|
||||
config,
|
||||
}
|
||||
.cell())
|
||||
}
|
||||
|
||||
// TODO(alexkirsz) This shouldn't be necessary. The loader tree should keep
|
||||
// track of this instead.
|
||||
fn get_original_page_name(pathname: &str) -> String {
|
||||
match pathname {
|
||||
"/" => "/page".to_string(),
|
||||
"/_not-found" => "/_not-found".to_string(),
|
||||
"/not-found" => "/not-found".to_string(),
|
||||
_ => format!("{}/page", pathname),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,6 @@ pub async fn get_app_route_entry(
|
|||
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();
|
||||
|
||||
let template_file = "build/templates/app-route.js";
|
||||
|
@ -83,7 +82,7 @@ pub async fn get_app_route_entry(
|
|||
)
|
||||
.replace(
|
||||
"\"VAR_ORIGINAL_PATHNAME\"",
|
||||
&StringifyJs(&original_page_name).to_string(),
|
||||
&StringifyJs(&original_name).to_string(),
|
||||
)
|
||||
.replace(
|
||||
"\"VAR_RESOLVED_PAGE_PATH\"",
|
||||
|
@ -132,8 +131,8 @@ pub async fn get_app_route_entry(
|
|||
};
|
||||
|
||||
Ok(AppEntry {
|
||||
pathname: pathname.to_string(),
|
||||
original_name: original_page_name,
|
||||
pathname,
|
||||
original_name,
|
||||
rsc_entry,
|
||||
config,
|
||||
}
|
||||
|
@ -177,10 +176,3 @@ pub async fn wrap_edge_entry(
|
|||
Value::new(ReferenceType::Internal(Vc::cell(inner_assets))),
|
||||
))
|
||||
}
|
||||
|
||||
fn get_original_route_name(pathname: &str) -> String {
|
||||
match pathname {
|
||||
"/" => "/route".to_string(),
|
||||
_ => format!("{}/route", pathname),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
//! (partial) Rust port of the `next-metadata-image-loader`
|
||||
//!
|
||||
//! See `next/src/build/webpack/loaders/next-metadata-image-loader`
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use indoc::formatdoc;
|
||||
use turbo_tasks::{ValueToString, Vc};
|
||||
use turbo_tasks_fs::{File, FileContent, FileSystemPath};
|
||||
use turbopack_binding::{
|
||||
turbo::tasks_hash::hash_xxh3_hash64,
|
||||
turbopack::{
|
||||
core::{
|
||||
asset::AssetContent,
|
||||
context::AssetContext,
|
||||
file_source::FileSource,
|
||||
module::Module,
|
||||
reference_type::{EcmaScriptModulesReferenceSubType, ReferenceType},
|
||||
source::Source,
|
||||
virtual_source::VirtualSource,
|
||||
},
|
||||
ecmascript::{
|
||||
chunk::{EcmascriptChunkPlaceable, EcmascriptExports},
|
||||
utils::StringifyJs,
|
||||
EcmascriptModuleAsset,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::next_app::AppPage;
|
||||
|
||||
async fn hash_file_content(path: Vc<FileSystemPath>) -> Result<u64> {
|
||||
let original_file_content = path.read().await?;
|
||||
|
||||
Ok(match &*original_file_content {
|
||||
FileContent::Content(content) => {
|
||||
let content = content.content().to_bytes()?;
|
||||
hash_xxh3_hash64(&*content)
|
||||
}
|
||||
FileContent::NotFound => {
|
||||
bail!("metadata file not found: {}", &path.to_string().await?);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
pub async fn dynamic_image_metadata_source(
|
||||
asset_context: Vc<Box<dyn AssetContext>>,
|
||||
path: Vc<FileSystemPath>,
|
||||
ty: String,
|
||||
page: AppPage,
|
||||
) -> Result<Vc<Box<dyn Source>>> {
|
||||
let stem = path.file_stem().await?;
|
||||
let stem = stem.as_deref().unwrap_or_default();
|
||||
let ext = &*path.extension().await?;
|
||||
|
||||
let hash_query = format!("?{:x}", hash_file_content(path).await?);
|
||||
|
||||
let use_numeric_sizes = ty == "twitter" || ty == "openGraph";
|
||||
let sizes = if use_numeric_sizes {
|
||||
"data.width = size.width; data.height = size.height;"
|
||||
} else {
|
||||
"data.sizes = size.width + \"x\" + size.height;"
|
||||
};
|
||||
|
||||
let source = Vc::upcast(FileSource::new(path));
|
||||
let exports = &*collect_direct_exports(asset_context.process(
|
||||
source,
|
||||
turbo_tasks::Value::new(ReferenceType::EcmaScriptModules(
|
||||
EcmaScriptModulesReferenceSubType::Undefined,
|
||||
)),
|
||||
))
|
||||
.await?;
|
||||
let exported_fields_excluding_default = exports
|
||||
.iter()
|
||||
.filter(|e| *e != "default")
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let code = formatdoc! {
|
||||
r#"
|
||||
import {{ {exported_fields_excluding_default} }} from {resource_path}
|
||||
import {{ fillMetadataSegment }} from 'next/dist/lib/metadata/get-metadata-route'
|
||||
|
||||
const imageModule = {{ {exported_fields_excluding_default} }}
|
||||
|
||||
export default async function (props) {{
|
||||
const {{ __metadata_id__: _, ...params }} = props.params
|
||||
const imageUrl = fillMetadataSegment({pathname_prefix}, params, {page_segment})
|
||||
|
||||
const {{ generateImageMetadata }} = imageModule
|
||||
|
||||
function getImageMetadata(imageMetadata, idParam) {{
|
||||
const data = {{
|
||||
alt: imageMetadata.alt,
|
||||
type: imageMetadata.contentType || 'image/png',
|
||||
url: imageUrl + (idParam ? ('/' + idParam) : '') + {hash_query},
|
||||
}}
|
||||
const {{ size }} = imageMetadata
|
||||
if (size) {{
|
||||
{sizes}
|
||||
}}
|
||||
return data
|
||||
}}
|
||||
|
||||
if (generateImageMetadata) {{
|
||||
const imageMetadataArray = await generateImageMetadata({{ params }})
|
||||
return imageMetadataArray.map((imageMetadata, index) => {{
|
||||
const idParam = (imageMetadata.id || index) + ''
|
||||
return getImageMetadata(imageMetadata, idParam)
|
||||
}})
|
||||
}} else {{
|
||||
return [getImageMetadata(imageModule, '')]
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
exported_fields_excluding_default = exported_fields_excluding_default,
|
||||
resource_path = StringifyJs(&format!("./{}.{}", stem, ext)),
|
||||
pathname_prefix = StringifyJs(&page.to_string()),
|
||||
page_segment = StringifyJs(stem),
|
||||
sizes = sizes,
|
||||
hash_query = StringifyJs(&hash_query),
|
||||
};
|
||||
|
||||
let file = File::from(code);
|
||||
let source = VirtualSource::new(
|
||||
path.parent().join(format!("{stem}--metadata.js")),
|
||||
AssetContent::file(file.into()),
|
||||
);
|
||||
|
||||
Ok(Vc::upcast(source))
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn collect_direct_exports(module: Vc<Box<dyn Module>>) -> Result<Vc<Vec<String>>> {
|
||||
let Some(ecmascript_asset) =
|
||||
Vc::try_resolve_downcast_type::<EcmascriptModuleAsset>(module).await?
|
||||
else {
|
||||
return Ok(Default::default());
|
||||
};
|
||||
|
||||
if let EcmascriptExports::EsmExports(exports) = &*ecmascript_asset.get_exports().await? {
|
||||
let exports = &*exports.await?;
|
||||
return Ok(Vc::cell(exports.exports.keys().cloned().collect()));
|
||||
}
|
||||
|
||||
Ok(Vc::cell(Vec::new()))
|
||||
}
|
341
packages/next-swc/crates/next-core/src/next_app/metadata/mod.rs
Normal file
341
packages/next-swc/crates/next-core/src/next_app/metadata/mod.rs
Normal file
|
@ -0,0 +1,341 @@
|
|||
use std::{collections::HashMap, ops::Deref};
|
||||
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::next_app::{AppPage, PageSegment, PageType};
|
||||
|
||||
pub mod image;
|
||||
pub mod route;
|
||||
|
||||
pub static STATIC_LOCAL_METADATA: Lazy<HashMap<&'static str, &'static [&'static str]>> =
|
||||
Lazy::new(|| {
|
||||
HashMap::from([
|
||||
(
|
||||
"icon",
|
||||
&["ico", "jpg", "jpeg", "png", "svg"] as &'static [&'static str],
|
||||
),
|
||||
("apple-icon", &["jpg", "jpeg", "png"]),
|
||||
("opengraph-image", &["jpg", "jpeg", "png", "gif"]),
|
||||
("twitter-image", &["jpg", "jpeg", "png", "gif"]),
|
||||
("sitemap", &["xml"]),
|
||||
])
|
||||
});
|
||||
|
||||
pub static STATIC_GLOBAL_METADATA: Lazy<HashMap<&'static str, &'static [&'static str]>> =
|
||||
Lazy::new(|| {
|
||||
HashMap::from([
|
||||
("favicon", &["ico"] as &'static [&'static str]),
|
||||
("manifest", &["webmanifest", "json"]),
|
||||
("robots", &["txt"]),
|
||||
])
|
||||
});
|
||||
|
||||
pub struct MetadataFileMatch<'a> {
|
||||
pub metadata_type: &'a str,
|
||||
pub number: Option<u32>,
|
||||
pub dynamic: bool,
|
||||
}
|
||||
|
||||
fn match_numbered_metadata(stem: &str) -> Option<(&str, &str)> {
|
||||
let (_whole, stem, number) = lazy_regex::regex_captures!(
|
||||
"^(icon|apple-icon|opengraph-image|twitter-image)(\\d+)$",
|
||||
stem
|
||||
)?;
|
||||
|
||||
Some((stem, number))
|
||||
}
|
||||
|
||||
fn match_metadata_file<'a>(
|
||||
filename: &'a str,
|
||||
page_extensions: &[String],
|
||||
metadata: &HashMap<&str, &[&str]>,
|
||||
) -> Option<MetadataFileMatch<'a>> {
|
||||
let (stem, ext) = filename.split_once('.')?;
|
||||
|
||||
let (stem, number) = match match_numbered_metadata(stem) {
|
||||
Some((stem, number)) => {
|
||||
let number: u32 = number.parse().ok()?;
|
||||
(stem, Some(number))
|
||||
}
|
||||
_ => (stem, None),
|
||||
};
|
||||
|
||||
let exts = metadata.get(stem)?;
|
||||
|
||||
// favicon can't be dynamic
|
||||
if stem != "favicon" && page_extensions.iter().any(|e| e == ext) {
|
||||
return Some(MetadataFileMatch {
|
||||
metadata_type: stem,
|
||||
number,
|
||||
dynamic: true,
|
||||
});
|
||||
}
|
||||
|
||||
exts.contains(&ext).then_some(MetadataFileMatch {
|
||||
metadata_type: stem,
|
||||
number,
|
||||
dynamic: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn match_local_metadata_file<'a>(
|
||||
basename: &'a str,
|
||||
page_extensions: &[String],
|
||||
) -> Option<MetadataFileMatch<'a>> {
|
||||
match_metadata_file(basename, page_extensions, STATIC_LOCAL_METADATA.deref())
|
||||
}
|
||||
|
||||
pub struct GlobalMetadataFileMatch<'a> {
|
||||
pub metadata_type: &'a str,
|
||||
pub dynamic: bool,
|
||||
}
|
||||
|
||||
pub fn match_global_metadata_file<'a>(
|
||||
basename: &'a str,
|
||||
page_extensions: &[String],
|
||||
) -> Option<GlobalMetadataFileMatch<'a>> {
|
||||
match_metadata_file(basename, page_extensions, STATIC_GLOBAL_METADATA.deref()).map(|m| {
|
||||
GlobalMetadataFileMatch {
|
||||
metadata_type: m.metadata_type,
|
||||
dynamic: m.dynamic,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn split_directory(path: &str) -> (Option<&str>, &str) {
|
||||
if let Some((dir, basename)) = path.rsplit_once('/') {
|
||||
if dir.is_empty() {
|
||||
return (Some("/"), basename);
|
||||
}
|
||||
|
||||
(Some(dir), basename)
|
||||
} else {
|
||||
(None, path)
|
||||
}
|
||||
}
|
||||
|
||||
fn filename(path: &str) -> &str {
|
||||
split_directory(path).1
|
||||
}
|
||||
|
||||
fn split_extension(path: &str) -> (&str, Option<&str>) {
|
||||
let filename = filename(path);
|
||||
if let Some((filename_before_extension, ext)) = filename.rsplit_once('.') {
|
||||
if filename_before_extension.is_empty() {
|
||||
return (filename, None);
|
||||
}
|
||||
|
||||
(filename_before_extension, Some(ext))
|
||||
} else {
|
||||
(filename, None)
|
||||
}
|
||||
}
|
||||
|
||||
fn file_stem(path: &str) -> &str {
|
||||
split_extension(path).0
|
||||
}
|
||||
|
||||
/// When you only pass the file extension as `[]`, it will only match the static
|
||||
/// convention files e.g. `/robots.txt`, `/sitemap.xml`, `/favicon.ico`,
|
||||
/// `/manifest.json`.
|
||||
///
|
||||
/// When you pass the file extension as `['js', 'jsx', 'ts',
|
||||
/// 'tsx']`, it will also match the dynamic convention files e.g. /robots.js,
|
||||
/// /sitemap.tsx, /favicon.jsx, /manifest.ts.
|
||||
///
|
||||
/// When `withExtension` is false, it will match the static convention files
|
||||
/// without the extension, by default it's true e.g. /robots, /sitemap,
|
||||
/// /favicon, /manifest, use to match dynamic API routes like app/robots.ts.
|
||||
pub fn is_metadata_route_file(
|
||||
app_dir_relative_path: &str,
|
||||
page_extensions: &[String],
|
||||
with_extension: bool,
|
||||
) -> bool {
|
||||
let (dir, filename) = split_directory(app_dir_relative_path);
|
||||
|
||||
if with_extension {
|
||||
if match_local_metadata_file(filename, page_extensions).is_some() {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
let stem = file_stem(filename);
|
||||
let stem = match_numbered_metadata(stem)
|
||||
.map(|(stem, _)| stem)
|
||||
.unwrap_or(stem);
|
||||
|
||||
if STATIC_LOCAL_METADATA.contains_key(stem) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if dir != Some("/") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if with_extension {
|
||||
if match_global_metadata_file(filename, page_extensions).is_some() {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
let base_name = file_stem(filename);
|
||||
if STATIC_GLOBAL_METADATA.contains_key(base_name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub fn is_static_metadata_route_file(app_dir_relative_path: &str) -> bool {
|
||||
is_metadata_route_file(app_dir_relative_path, &[], true)
|
||||
}
|
||||
|
||||
/// Remove the 'app' prefix or '/route' suffix, only check the route name since
|
||||
/// they're only allowed in root app directory
|
||||
///
|
||||
/// e.g.
|
||||
/// - /app/robots -> /robots
|
||||
/// - app/robots -> /robots
|
||||
/// - /robots -> /robots
|
||||
pub fn is_metadata_route(mut route: &str) -> bool {
|
||||
if let Some(stripped) = route.strip_prefix("/app/") {
|
||||
route = stripped;
|
||||
} else if let Some(stripped) = route.strip_prefix("app/") {
|
||||
route = stripped;
|
||||
}
|
||||
|
||||
if let Some(stripped) = route.strip_suffix("/route") {
|
||||
route = stripped;
|
||||
}
|
||||
|
||||
let mut page = route.to_string();
|
||||
if !page.starts_with('/') {
|
||||
page = format!("/{}", page);
|
||||
}
|
||||
|
||||
!page.ends_with("/page") && is_metadata_route_file(&page, &[], false)
|
||||
}
|
||||
|
||||
/// http://www.cse.yorku.ca/~oz/hash.html
|
||||
fn djb2_hash(str: &str) -> u32 {
|
||||
str.chars().fold(5381, |hash, c| {
|
||||
((hash << 5).wrapping_add(hash)).wrapping_add(c as u32) // hash * 33 + c
|
||||
})
|
||||
}
|
||||
|
||||
// this is here to mirror next.js behaviour.
|
||||
fn format_radix(mut x: u32, radix: u32) -> String {
|
||||
let mut result = vec![];
|
||||
|
||||
loop {
|
||||
let m = x % radix;
|
||||
x /= radix;
|
||||
|
||||
// will panic if you use a bad radix (< 2 or > 36).
|
||||
result.push(std::char::from_digit(m, radix).unwrap());
|
||||
if x == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result.into_iter().rev().collect()
|
||||
}
|
||||
|
||||
/// If there's special convention like (...) or @ in the page path,
|
||||
/// Give it a unique hash suffix to avoid conflicts
|
||||
///
|
||||
/// e.g.
|
||||
/// /app/open-graph.tsx -> /open-graph/route
|
||||
/// /app/(post)/open-graph.tsx -> /open-graph/route-[0-9a-z]{6}
|
||||
fn get_metadata_route_suffix(page: &str) -> Option<String> {
|
||||
if (page.contains('(') && page.contains(')')) || page.contains('@') {
|
||||
Some(format_radix(djb2_hash(page), 36))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Map metadata page key to the corresponding route
|
||||
///
|
||||
/// static file page key: /app/robots.txt -> /robots.txt -> /robots.txt/route
|
||||
/// dynamic route page key: /app/robots.tsx -> /robots -> /robots.txt/route
|
||||
pub fn normalize_metadata_route(mut page: AppPage) -> Result<AppPage> {
|
||||
if !is_metadata_route(&format!("{page}")) {
|
||||
return Ok(page);
|
||||
}
|
||||
|
||||
let mut route = page.to_string();
|
||||
let mut suffix: Option<String> = None;
|
||||
if route == "/robots" {
|
||||
route += ".txt"
|
||||
} else if route == "/manifest" {
|
||||
route += ".webmanifest"
|
||||
} else if route.ends_with("/sitemap") {
|
||||
route += ".xml"
|
||||
} else {
|
||||
// Remove the file extension, e.g. /route-path/robots.txt -> /route-path
|
||||
let pathname_prefix = split_directory(&route).0.unwrap_or_default();
|
||||
suffix = get_metadata_route_suffix(pathname_prefix);
|
||||
}
|
||||
|
||||
// Support both /<metadata-route.ext> and custom routes
|
||||
// /<metadata-route>/route.ts. If it's a metadata file route, we need to
|
||||
// append /[id]/route to the page.
|
||||
if !route.ends_with("/route") {
|
||||
let is_static_metadata_file = is_static_metadata_route_file(&route);
|
||||
let (base_name, ext) = split_extension(&route);
|
||||
|
||||
let is_static_route = route.starts_with("/robots")
|
||||
|| route.starts_with("/manifest")
|
||||
|| is_static_metadata_file;
|
||||
|
||||
page.0.pop();
|
||||
|
||||
page.push(PageSegment::Static(format!(
|
||||
"{}{}{}",
|
||||
base_name,
|
||||
suffix
|
||||
.map(|suffix| format!("-{suffix}"))
|
||||
.unwrap_or_default(),
|
||||
ext.map(|ext| format!(".{ext}")).unwrap_or_default(),
|
||||
)))?;
|
||||
|
||||
if !is_static_route {
|
||||
page.push(PageSegment::OptionalCatchAll("__metadata_id__".to_string()))?;
|
||||
}
|
||||
|
||||
page.push(PageSegment::PageType(PageType::Route))?;
|
||||
}
|
||||
|
||||
Ok(page)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::normalize_metadata_route;
|
||||
use crate::next_app::AppPage;
|
||||
|
||||
#[test]
|
||||
fn test_normalize_metadata_route() {
|
||||
let cases = vec![
|
||||
[
|
||||
"/client/(meme)/more-route/twitter-image",
|
||||
"/client/(meme)/more-route/twitter-image-769mad/[[...__metadata_id__]]/route",
|
||||
],
|
||||
[
|
||||
"/client/(meme)/more-route/twitter-image2",
|
||||
"/client/(meme)/more-route/twitter-image2-769mad/[[...__metadata_id__]]/route",
|
||||
],
|
||||
["/robots.txt", "/robots.txt/route"],
|
||||
["/manifest.webmanifest", "/manifest.webmanifest/route"],
|
||||
];
|
||||
|
||||
for [input, expected] in cases {
|
||||
let page = AppPage::parse(input).unwrap();
|
||||
let normalized = normalize_metadata_route(page).unwrap();
|
||||
|
||||
assert_eq!(&normalized.to_string(), expected);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,364 @@
|
|||
//! Rust port of the `next-metadata-route-loader`
|
||||
//!
|
||||
//! See `next/src/build/webpack/loaders/next-metadata-route-loader`
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use base64::{display::Base64Display, engine::general_purpose::STANDARD};
|
||||
use indoc::{formatdoc, indoc};
|
||||
use turbo_tasks::{ValueToString, Vc};
|
||||
use turbopack_binding::{
|
||||
turbo::tasks_fs::{File, FileContent, FileSystemPath},
|
||||
turbopack::{
|
||||
core::{asset::AssetContent, source::Source, virtual_source::VirtualSource},
|
||||
ecmascript::utils::StringifyJs,
|
||||
turbopack::ModuleAssetContext,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_structure::MetadataItem,
|
||||
mode::NextMode,
|
||||
next_app::{app_entry::AppEntry, app_route_entry::get_app_route_entry, AppPage, PageSegment},
|
||||
};
|
||||
|
||||
/// Computes the route source for a Next.js metadata file.
|
||||
#[turbo_tasks::function]
|
||||
pub async fn get_app_metadata_route_source(
|
||||
page: AppPage,
|
||||
mode: NextMode,
|
||||
metadata: MetadataItem,
|
||||
) -> Result<Vc<Box<dyn Source>>> {
|
||||
Ok(match metadata {
|
||||
MetadataItem::Static { path } => static_route_source(mode, path),
|
||||
MetadataItem::Dynamic { path } => {
|
||||
let stem = path.file_stem().await?;
|
||||
let stem = stem.as_deref().unwrap_or_default();
|
||||
|
||||
if stem == "robots" || stem == "manifest" {
|
||||
dynamic_text_route_source(path)
|
||||
} else if stem == "sitemap" {
|
||||
dynamic_site_map_route_source(mode, path, page)
|
||||
} else {
|
||||
dynamic_image_route_source(path)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
pub fn get_app_metadata_route_entry(
|
||||
nodejs_context: Vc<ModuleAssetContext>,
|
||||
edge_context: Vc<ModuleAssetContext>,
|
||||
project_root: Vc<FileSystemPath>,
|
||||
page: AppPage,
|
||||
mode: NextMode,
|
||||
metadata: MetadataItem,
|
||||
) -> Vc<AppEntry> {
|
||||
get_app_route_entry(
|
||||
nodejs_context,
|
||||
edge_context,
|
||||
get_app_metadata_route_source(page.clone(), mode, metadata),
|
||||
page,
|
||||
project_root,
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_content_type(path: Vc<FileSystemPath>) -> Result<String> {
|
||||
let stem = &*path.file_stem().await?;
|
||||
let ext = &*path.extension().await?;
|
||||
|
||||
let name = stem.as_deref().unwrap_or_default();
|
||||
let mut ext = ext.as_str();
|
||||
if ext == "jpg" {
|
||||
ext = "jpeg"
|
||||
}
|
||||
|
||||
if name == "favicon" && ext == "ico" {
|
||||
return Ok("image/x-icon".to_string());
|
||||
}
|
||||
if name == "sitemap" {
|
||||
return Ok("application/xml".to_string());
|
||||
}
|
||||
if name == "robots" {
|
||||
return Ok("text/plain".to_string());
|
||||
}
|
||||
if name == "manifest" {
|
||||
return Ok("application/manifest+json".to_string());
|
||||
}
|
||||
|
||||
if ext == "png" || ext == "jpeg" || ext == "ico" || ext == "svg" {
|
||||
return Ok(mime_guess::from_ext(ext)
|
||||
.first_or_octet_stream()
|
||||
.to_string());
|
||||
}
|
||||
|
||||
Ok("text/plain".to_string())
|
||||
}
|
||||
|
||||
const CACHE_HEADER_NONE: &str = "no-cache, no-store";
|
||||
const CACHE_HEADER_LONG_CACHE: &str = "public, immutable, no-transform, max-age=31536000";
|
||||
const CACHE_HEADER_REVALIDATE: &str = "public, max-age=0, must-revalidate";
|
||||
|
||||
async fn get_base64_file_content(path: Vc<FileSystemPath>) -> Result<String> {
|
||||
let original_file_content = path.read().await?;
|
||||
|
||||
Ok(match &*original_file_content {
|
||||
FileContent::Content(content) => {
|
||||
let content = content.content().to_bytes()?;
|
||||
Base64Display::new(&content, &STANDARD).to_string()
|
||||
}
|
||||
FileContent::NotFound => {
|
||||
bail!("metadata file not found: {}", &path.to_string().await?);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn static_route_source(
|
||||
mode: NextMode,
|
||||
path: Vc<FileSystemPath>,
|
||||
) -> Result<Vc<Box<dyn Source>>> {
|
||||
let stem = path.file_stem().await?;
|
||||
let stem = stem.as_deref().unwrap_or_default();
|
||||
|
||||
let content_type = get_content_type(path).await?;
|
||||
|
||||
let cache_control = if stem == "favicon" {
|
||||
CACHE_HEADER_REVALIDATE
|
||||
} else if mode == NextMode::Build {
|
||||
CACHE_HEADER_LONG_CACHE
|
||||
} else {
|
||||
CACHE_HEADER_NONE
|
||||
};
|
||||
|
||||
let original_file_content_b64 = get_base64_file_content(path).await?;
|
||||
|
||||
let code = formatdoc! {
|
||||
r#"
|
||||
import {{ NextResponse }} from 'next/server'
|
||||
|
||||
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: {{
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': cacheControl,
|
||||
}},
|
||||
}})
|
||||
}}
|
||||
|
||||
export const dynamic = 'force-static'
|
||||
"#,
|
||||
content_type = StringifyJs(&content_type),
|
||||
cache_control = StringifyJs(cache_control),
|
||||
original_file_content_b64 = StringifyJs(&original_file_content_b64),
|
||||
};
|
||||
|
||||
let file = File::from(code);
|
||||
let source = VirtualSource::new(
|
||||
path.parent().join(format!("{stem}--route-entry.js")),
|
||||
AssetContent::file(file.into()),
|
||||
);
|
||||
|
||||
Ok(Vc::upcast(source))
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn dynamic_text_route_source(path: Vc<FileSystemPath>) -> Result<Vc<Box<dyn Source>>> {
|
||||
let stem = path.file_stem().await?;
|
||||
let stem = stem.as_deref().unwrap_or_default();
|
||||
let ext = &*path.extension().await?;
|
||||
|
||||
let content_type = get_content_type(path).await?;
|
||||
|
||||
let code = formatdoc! {
|
||||
r#"
|
||||
import {{ NextResponse }} from 'next/server'
|
||||
import handler from {resource_path}
|
||||
import {{ resolveRouteData }} from
|
||||
'next/dist/build/webpack/loaders/metadata/resolve-route-data'
|
||||
|
||||
const contentType = {content_type}
|
||||
const cacheControl = {cache_control}
|
||||
const fileType = {file_type}
|
||||
|
||||
export async function GET() {{
|
||||
const data = await handler()
|
||||
const content = resolveRouteData(data, fileType)
|
||||
|
||||
return new NextResponse(content, {{
|
||||
headers: {{
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': cacheControl,
|
||||
}},
|
||||
}})
|
||||
}}
|
||||
"#,
|
||||
resource_path = StringifyJs(&format!("./{}.{}", stem, ext)),
|
||||
content_type = StringifyJs(&content_type),
|
||||
file_type = StringifyJs(&stem),
|
||||
cache_control = StringifyJs(CACHE_HEADER_REVALIDATE),
|
||||
};
|
||||
|
||||
let file = File::from(code);
|
||||
let source = VirtualSource::new(
|
||||
path.parent().join(format!("{stem}--route-entry.js")),
|
||||
AssetContent::file(file.into()),
|
||||
);
|
||||
|
||||
Ok(Vc::upcast(source))
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn dynamic_site_map_route_source(
|
||||
mode: NextMode,
|
||||
path: Vc<FileSystemPath>,
|
||||
page: AppPage,
|
||||
) -> Result<Vc<Box<dyn Source>>> {
|
||||
let stem = path.file_stem().await?;
|
||||
let stem = stem.as_deref().unwrap_or_default();
|
||||
let ext = &*path.extension().await?;
|
||||
|
||||
let content_type = get_content_type(path).await?;
|
||||
|
||||
let mut static_generation_code = "";
|
||||
|
||||
if mode == NextMode::Build
|
||||
&& page.contains(&PageSegment::Dynamic("[__metadata_id__]".to_string()))
|
||||
{
|
||||
static_generation_code = indoc! {
|
||||
r#"
|
||||
export async function generateStaticParams() {
|
||||
const sitemaps = await generateSitemaps()
|
||||
const params = []
|
||||
|
||||
for (const item of sitemaps) {
|
||||
params.push({ __metadata_id__: item.id.toString() + '.xml' })
|
||||
}
|
||||
return params
|
||||
}
|
||||
"#,
|
||||
};
|
||||
}
|
||||
|
||||
let code = formatdoc! {
|
||||
r#"
|
||||
import {{ NextResponse }} from 'next/server'
|
||||
import * as _sitemapModule from {resource_path}
|
||||
import {{ resolveRouteData }} from 'next/dist/build/webpack/loaders/metadata/resolve-route-data'
|
||||
|
||||
const sitemapModule = {{ ..._sitemapModule }}
|
||||
const handler = sitemapModule.default
|
||||
const generateSitemaps = sitemapModule.generateSitemaps
|
||||
const contentType = {content_type}
|
||||
const cacheControl = {cache_control}
|
||||
const fileType = {file_type}
|
||||
|
||||
export async function GET(_, ctx) {{
|
||||
const {{ __metadata_id__ = [], ...params }} = ctx.params || {{}}
|
||||
const targetId = __metadata_id__[0]
|
||||
let id = undefined
|
||||
const sitemaps = generateSitemaps ? await generateSitemaps() : null
|
||||
|
||||
if (sitemaps) {{
|
||||
id = sitemaps.find((item) => {{
|
||||
if (process.env.NODE_ENV !== 'production') {{
|
||||
if (item?.id == null) {{
|
||||
throw new Error('id property is required for every item returned from generateSitemaps')
|
||||
}}
|
||||
}}
|
||||
return item.id.toString() === targetId
|
||||
}})?.id
|
||||
|
||||
if (id == null) {{
|
||||
return new NextResponse('Not Found', {{
|
||||
status: 404,
|
||||
}})
|
||||
}}
|
||||
}}
|
||||
|
||||
const data = await handler({{ id }})
|
||||
const content = resolveRouteData(data, fileType)
|
||||
|
||||
return new NextResponse(content, {{
|
||||
headers: {{
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': cacheControl,
|
||||
}},
|
||||
}})
|
||||
}}
|
||||
|
||||
{static_generation_code}
|
||||
"#,
|
||||
resource_path = StringifyJs(&format!("./{}.{}", stem, ext)),
|
||||
content_type = StringifyJs(&content_type),
|
||||
file_type = StringifyJs(&stem),
|
||||
cache_control = StringifyJs(CACHE_HEADER_REVALIDATE),
|
||||
static_generation_code = static_generation_code,
|
||||
};
|
||||
|
||||
let file = File::from(code);
|
||||
let source = VirtualSource::new(
|
||||
path.parent().join(format!("{stem}--route-entry.js")),
|
||||
AssetContent::file(file.into()),
|
||||
);
|
||||
|
||||
Ok(Vc::upcast(source))
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn dynamic_image_route_source(path: Vc<FileSystemPath>) -> Result<Vc<Box<dyn Source>>> {
|
||||
let stem = path.file_stem().await?;
|
||||
let stem = stem.as_deref().unwrap_or_default();
|
||||
let ext = &*path.extension().await?;
|
||||
|
||||
let code = formatdoc! {
|
||||
r#"
|
||||
import {{ NextResponse }} from 'next/server'
|
||||
import * as _imageModule from {resource_path}
|
||||
|
||||
const imageModule = {{ ..._imageModule }}
|
||||
|
||||
const handler = imageModule.default
|
||||
const generateImageMetadata = imageModule.generateImageMetadata
|
||||
|
||||
export async function GET(_, ctx) {{
|
||||
const {{ __metadata_id__ = [], ...params }} = ctx.params || {{}}
|
||||
const targetId = __metadata_id__[0]
|
||||
let id = undefined
|
||||
const imageMetadata = generateImageMetadata ? await generateImageMetadata({{ params }}) : null
|
||||
|
||||
if (imageMetadata) {{
|
||||
id = imageMetadata.find((item) => {{
|
||||
if (process.env.NODE_ENV !== 'production') {{
|
||||
if (item?.id == null) {{
|
||||
throw new Error('id property is required for every item returned from generateImageMetadata')
|
||||
}}
|
||||
}}
|
||||
return item.id.toString() === targetId
|
||||
}})?.id
|
||||
|
||||
if (id == null) {{
|
||||
return new NextResponse('Not Found', {{
|
||||
status: 404,
|
||||
}})
|
||||
}}
|
||||
}}
|
||||
|
||||
return handler({{ params: ctx.params ? params : undefined, id }})
|
||||
}}
|
||||
"#,
|
||||
resource_path = StringifyJs(&format!("./{}.{}", stem, ext)),
|
||||
};
|
||||
|
||||
let file = File::from(code);
|
||||
let source = VirtualSource::new(
|
||||
path.parent().join(format!("{stem}--route-entry.js")),
|
||||
AssetContent::file(file.into()),
|
||||
);
|
||||
|
||||
Ok(Vc::upcast(source))
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
pub(crate) mod app_client_references_chunks;
|
||||
pub(crate) mod app_client_shared_chunks;
|
||||
pub(crate) mod app_entry;
|
||||
pub(crate) mod app_favicon_entry;
|
||||
pub(crate) mod app_page_entry;
|
||||
pub(crate) mod app_route_entry;
|
||||
pub(crate) mod unsupported_dynamic_metadata_issue;
|
||||
pub mod app_client_references_chunks;
|
||||
pub mod app_client_shared_chunks;
|
||||
pub mod app_entry;
|
||||
pub mod app_page_entry;
|
||||
pub mod app_route_entry;
|
||||
pub mod metadata;
|
||||
|
||||
use std::{
|
||||
fmt::{Display, Formatter, Write},
|
||||
|
@ -21,20 +20,27 @@ pub use crate::next_app::{
|
|||
},
|
||||
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,
|
||||
};
|
||||
|
||||
/// See [AppPage].
|
||||
#[derive(Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq, TaskInput, TraceRawVcs)]
|
||||
pub enum PageSegment {
|
||||
/// e.g. `/dashboard`
|
||||
Static(String),
|
||||
/// e.g. `/[id]`
|
||||
Dynamic(String),
|
||||
/// e.g. `/[...slug]`
|
||||
CatchAll(String),
|
||||
/// e.g. `/[[...slug]]`
|
||||
OptionalCatchAll(String),
|
||||
/// e.g. `/(shop)`
|
||||
Group(String),
|
||||
/// e.g. `/@auth`
|
||||
Parallel(String),
|
||||
/// The final page type appended. (e.g. `/dashboard/page`,
|
||||
/// `/api/hello/route`)
|
||||
PageType(PageType),
|
||||
}
|
||||
|
||||
|
@ -151,6 +157,13 @@ impl AppPage {
|
|||
)
|
||||
}
|
||||
|
||||
if self.is_complete() {
|
||||
bail!(
|
||||
"Invalid segment {}, this page path already has the final PageType appended",
|
||||
segment
|
||||
)
|
||||
}
|
||||
|
||||
self.0.push(segment);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -184,6 +197,18 @@ impl AppPage {
|
|||
|
||||
Ok(app_page)
|
||||
}
|
||||
|
||||
pub fn is_root(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
|
||||
pub fn is_complete(&self) -> bool {
|
||||
matches!(self.0.last(), Some(PageSegment::PageType(..)))
|
||||
}
|
||||
|
||||
pub fn complete(self, page_type: PageType) -> Result<Self> {
|
||||
self.clone_push(PageSegment::PageType(page_type))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AppPage {
|
||||
|
@ -209,11 +234,18 @@ impl Deref for AppPage {
|
|||
}
|
||||
}
|
||||
|
||||
/// Path segments for a router path (not including parallel routes and groups).
|
||||
///
|
||||
/// Also see [AppPath].
|
||||
#[derive(Clone, Debug, Hash, Serialize, Deserialize, PartialEq, Eq, TaskInput, TraceRawVcs)]
|
||||
pub enum PathSegment {
|
||||
/// e.g. `/dashboard`
|
||||
Static(String),
|
||||
/// e.g. `/[id]`
|
||||
Dynamic(String),
|
||||
/// e.g. `/[...slug]`
|
||||
CatchAll(String),
|
||||
/// e.g. `/[[...slug]]`
|
||||
OptionalCatchAll(String),
|
||||
}
|
||||
|
||||
|
@ -240,7 +272,11 @@ impl Display for PathSegment {
|
|||
}
|
||||
}
|
||||
|
||||
/// The pathname (including dynamic placeholders) for a route to resolve.
|
||||
/// The pathname (including dynamic placeholders) for the next.js router to
|
||||
/// resolve.
|
||||
///
|
||||
/// Does not include internal modifiers as it's the equivalent of the http
|
||||
/// request path.
|
||||
#[derive(
|
||||
Clone, Debug, Hash, PartialEq, Eq, Default, Serialize, Deserialize, TaskInput, TraceRawVcs,
|
||||
)]
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
use anyhow::Result;
|
||||
use turbo_tasks::{TryJoinIterExt, ValueToString, Vc};
|
||||
use turbo_tasks_fs::FileSystemPath;
|
||||
use turbopack_binding::turbopack::{
|
||||
core::issue::{Issue, IssueSeverity},
|
||||
ecmascript::utils::FormatIter,
|
||||
};
|
||||
|
||||
#[turbo_tasks::value(shared)]
|
||||
pub struct UnsupportedDynamicMetadataIssue {
|
||||
pub app_dir: Vc<FileSystemPath>,
|
||||
pub files: Vec<Vc<FileSystemPath>>,
|
||||
}
|
||||
|
||||
#[turbo_tasks::value_impl]
|
||||
impl Issue for UnsupportedDynamicMetadataIssue {
|
||||
#[turbo_tasks::function]
|
||||
fn severity(&self) -> Vc<IssueSeverity> {
|
||||
IssueSeverity::Warning.into()
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
fn category(&self) -> Vc<String> {
|
||||
Vc::cell("unsupported".to_string())
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
fn file_path(&self) -> Vc<FileSystemPath> {
|
||||
self.app_dir
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
fn title(&self) -> Vc<String> {
|
||||
Vc::cell(
|
||||
"Dynamic metadata from filesystem is currently not supported in Turbopack".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn description(&self) -> Result<Vc<String>> {
|
||||
let mut files = self
|
||||
.files
|
||||
.iter()
|
||||
.map(|file| file.to_string())
|
||||
.try_join()
|
||||
.await?;
|
||||
files.sort();
|
||||
Ok(Vc::cell(format!(
|
||||
"The following files were found in the app directory, but are not supported by \
|
||||
Turbopack. They are ignored:\n{}",
|
||||
FormatIter(|| files.iter().flat_map(|file| vec!["\n- ", file]))
|
||||
)))
|
||||
}
|
||||
}
|
|
@ -215,7 +215,14 @@ pub async fn get_next_server_import_map(
|
|||
|
||||
let ty = ty.into_value();
|
||||
|
||||
insert_next_server_special_aliases(&mut import_map, ty, mode, NextRuntime::NodeJs).await?;
|
||||
insert_next_server_special_aliases(
|
||||
&mut import_map,
|
||||
project_path,
|
||||
ty,
|
||||
mode,
|
||||
NextRuntime::NodeJs,
|
||||
)
|
||||
.await?;
|
||||
let external: Vc<ImportMapping> = ImportMapping::External(None).cell();
|
||||
|
||||
import_map.insert_exact_alias("next/dist/server/require-hook", external);
|
||||
|
@ -290,7 +297,8 @@ pub async fn get_next_edge_import_map(
|
|||
|
||||
let ty = ty.into_value();
|
||||
|
||||
insert_next_server_special_aliases(&mut import_map, ty, mode, NextRuntime::Edge).await?;
|
||||
insert_next_server_special_aliases(&mut import_map, project_path, ty, mode, NextRuntime::Edge)
|
||||
.await?;
|
||||
|
||||
match ty {
|
||||
ServerContextType::Pages { .. } | ServerContextType::PagesData { .. } => {}
|
||||
|
@ -371,6 +379,7 @@ static NEXT_ALIASES: [(&str, &str); 23] = [
|
|||
|
||||
async fn insert_next_server_special_aliases(
|
||||
import_map: &mut ImportMap,
|
||||
project_path: Vc<FileSystemPath>,
|
||||
ty: ServerContextType,
|
||||
mode: NextMode,
|
||||
runtime: NextRuntime,
|
||||
|
@ -553,6 +562,14 @@ async fn insert_next_server_special_aliases(
|
|||
(_, ServerContextType::Middleware) => {}
|
||||
}
|
||||
|
||||
import_map.insert_exact_alias(
|
||||
"@vercel/og",
|
||||
external_if_node(
|
||||
project_path,
|
||||
"next/dist/server/web/spec-extension/image-response",
|
||||
),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -552,6 +552,7 @@ async fn get_mock_server_future(mock_dir: &Path) -> Result<(), String> {
|
|||
Some(mock_dir.to_path_buf()),
|
||||
false,
|
||||
0,
|
||||
std::future::pending(),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
|
|
|
@ -17,27 +17,22 @@ export default function Test() {
|
|||
).toEqual([
|
||||
expect.objectContaining({
|
||||
rel: 'manifest',
|
||||
href: expect.stringMatching(/^\/_next\/static\/.+\.webmanifest$/),
|
||||
href: expect.stringMatching(/^\/manifest\.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$/),
|
||||
href: expect.stringMatching(/^\/icon\d+\.png\?.+$/),
|
||||
sizes: '32x32',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
rel: 'icon',
|
||||
href: expect.stringMatching(/^\/_next\/static\/.+\.png$/),
|
||||
href: expect.stringMatching(/^\/icon\d+\.png\?.+$/),
|
||||
sizes: '64x64',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
rel: 'apple-touch-icon',
|
||||
href: expect.stringMatching(/^\/_next\/static\/.+\.png$/),
|
||||
href: expect.stringMatching(/^\/apple-icon\.png\?.+$/),
|
||||
sizes: '114x114',
|
||||
}),
|
||||
])
|
||||
|
@ -51,7 +46,7 @@ export default function Test() {
|
|||
.map((l) => [l.getAttribute('property'), l.getAttribute('content')])
|
||||
)
|
||||
expect(metaObject).toEqual({
|
||||
'og:image': expect.stringMatching(/^.+\/_next\/static\/.+\.png$/),
|
||||
'og:image': expect.stringMatching(/^.+\/opengraph-image\.png\?.+$/),
|
||||
'og:image:width': '114',
|
||||
'og:image:height': '114',
|
||||
'og:image:alt': 'This is an alt text.',
|
||||
|
|
|
@ -103,6 +103,7 @@ import {
|
|||
TurboPackConnectedAction,
|
||||
} from '../../dev/hot-reloader-types'
|
||||
import { debounce } from '../../utils'
|
||||
import { normalizeMetadataRoute } from '../../../lib/metadata/get-metadata-route'
|
||||
|
||||
const wsServer = new ws.Server({ noServer: true })
|
||||
|
||||
|
@ -230,7 +231,7 @@ async function startWatcher(opts: SetupOpts) {
|
|||
}
|
||||
|
||||
function formatIssue(issue: Issue) {
|
||||
const { filePath, title, description, source } = issue
|
||||
const { filePath, title, description, source, detail } = issue
|
||||
let formattedTitle = title.replace(/\n/g, '\n ')
|
||||
let message = ''
|
||||
|
||||
|
@ -265,6 +266,9 @@ async function startWatcher(opts: SetupOpts) {
|
|||
if (description) {
|
||||
message += `\n${description.replace(/\n/g, '\n ')}`
|
||||
}
|
||||
if (detail) {
|
||||
message += `\n${detail.replace(/\n/g, '\n ')}`
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
@ -659,7 +663,7 @@ async function startWatcher(opts: SetupOpts) {
|
|||
|
||||
async function writeBuildManifest(): Promise<void> {
|
||||
const buildManifest = mergeBuildManifests(buildManifests.values())
|
||||
const buildManifestPath = path.join(distDir, 'build-manifest.json')
|
||||
const buildManifestPath = path.join(distDir, BUILD_MANIFEST)
|
||||
await clearCache(buildManifestPath)
|
||||
await writeFile(
|
||||
buildManifestPath,
|
||||
|
@ -896,6 +900,7 @@ async function startWatcher(opts: SetupOpts) {
|
|||
case 'client-success': // { clientId }
|
||||
case 'server-component-reload-page': // { clientId }
|
||||
case 'client-reload-page': // { clientId }
|
||||
case 'client-removed-page': // { page }
|
||||
case 'client-full-reload': // { stackTrace, hadRuntimeError }
|
||||
// TODO
|
||||
break
|
||||
|
@ -1012,7 +1017,13 @@ async function startWatcher(opts: SetupOpts) {
|
|||
}
|
||||
|
||||
await currentEntriesHandling
|
||||
const route = curEntries.get(page)
|
||||
const route =
|
||||
curEntries.get(page) ??
|
||||
curEntries.get(
|
||||
normalizeAppPath(
|
||||
normalizeMetadataRoute(match?.definition?.page ?? inputPage)
|
||||
)
|
||||
)
|
||||
|
||||
if (!route) {
|
||||
// TODO: why is this entry missing in turbopack?
|
||||
|
|
Loading…
Reference in a new issue