next_core/next_app/metadata/
image.rs1use anyhow::{Result, bail};
6use indoc::formatdoc;
7use turbo_rcstr::RcStr;
8use turbo_tasks::Vc;
9use turbo_tasks_fs::{File, FileContent, FileSystemPath};
10use turbo_tasks_hash::hash_xxh3_hash64;
11use turbopack_core::{
12 asset::AssetContent,
13 context::AssetContext,
14 file_source::FileSource,
15 module::Module,
16 reference_type::{EcmaScriptModulesReferenceSubType, ReferenceType},
17 source::Source,
18 virtual_source::VirtualSource,
19};
20use turbopack_ecmascript::{
21 chunk::{EcmascriptChunkPlaceable, EcmascriptExports},
22 utils::StringifyJs,
23};
24
25use crate::next_app::AppPage;
26
27async fn hash_file_content(path: FileSystemPath) -> Result<u64> {
28 let original_file_content = path.read().await?;
29
30 Ok(match &*original_file_content {
31 FileContent::Content(content) => {
32 let content = content.content().to_bytes();
33 hash_xxh3_hash64(&*content)
34 }
35 FileContent::NotFound => {
36 bail!(
37 "metadata file not found: {}",
38 &path.value_to_string().await?
39 );
40 }
41 })
42}
43
44#[turbo_tasks::function]
45pub async fn dynamic_image_metadata_source(
46 asset_context: Vc<Box<dyn AssetContext>>,
47 path: FileSystemPath,
48 ty: RcStr,
49 page: AppPage,
50) -> Result<Vc<Box<dyn Source>>> {
51 let stem = path.file_stem();
52 let stem = stem.unwrap_or_default();
53 let ext = path.extension();
54
55 let hash_query = format!("?{:x}", hash_file_content(path.clone()).await?);
56
57 let use_numeric_sizes = ty == "twitter" || ty == "openGraph";
58 let sizes = if use_numeric_sizes {
59 "data.width = size.width; data.height = size.height;".to_string()
60 } else {
61 let sizes = if ext == "svg" {
69 "any"
70 } else {
71 "${size.width}x${size.height}"
72 };
73
74 format!("data.sizes = `{sizes}`;")
75 };
76
77 let source = Vc::upcast(FileSource::new(path.clone()));
78 let module = asset_context
79 .process(
80 source,
81 ReferenceType::EcmaScriptModules(EcmaScriptModulesReferenceSubType::Undefined),
82 )
83 .module();
84 let exports = &*collect_direct_exports(module).await?;
85 let exported_fields_excluding_default = exports
86 .iter()
87 .filter(|e| *e != "default")
88 .cloned()
89 .collect::<Vec<_>>()
90 .join(", ");
91
92 let code = formatdoc! {
93 r#"
94 import {{ {exported_fields_excluding_default} }} from {resource_path}
95 import {{ fillMetadataSegment }} from 'next/dist/lib/metadata/get-metadata-route'
96
97 const imageModule = {{ {exported_fields_excluding_default} }}
98
99 export default async function (props) {{
100 const {{ __metadata_id__: _, ...params }} = await props.params
101 const imageUrl = fillMetadataSegment({pathname_prefix}, params, {page_segment})
102
103 const {{ generateImageMetadata }} = imageModule
104
105 function getImageMetadata(imageMetadata, idParam) {{
106 const data = {{
107 alt: imageMetadata.alt,
108 type: imageMetadata.contentType || 'image/png',
109 url: imageUrl + (idParam ? ('/' + idParam) : '') + {hash_query},
110 }}
111 const {{ size }} = imageMetadata
112 if (size) {{
113 {sizes}
114 }}
115 return data
116 }}
117
118 if (generateImageMetadata) {{
119 const imageMetadataArray = await generateImageMetadata({{ params }})
120 return imageMetadataArray.map((imageMetadata, index) => {{
121 const idParam = (imageMetadata.id || index) + ''
122 return getImageMetadata(imageMetadata, idParam)
123 }})
124 }} else {{
125 return [getImageMetadata(imageModule, '')]
126 }}
127 }}
128 "#,
129 exported_fields_excluding_default = exported_fields_excluding_default,
130 resource_path = StringifyJs(&format!("./{stem}.{ext}")),
131 pathname_prefix = StringifyJs(&page.to_string()),
132 page_segment = StringifyJs(stem),
133 sizes = sizes,
134 hash_query = StringifyJs(&hash_query),
135 };
136
137 let file = File::from(code);
138 let source = VirtualSource::new(
139 path.parent().join(&format!("{stem}--metadata.js"))?,
140 AssetContent::file(file.into()),
141 );
142
143 Ok(Vc::upcast(source))
144}
145
146#[turbo_tasks::function]
147async fn collect_direct_exports(module: Vc<Box<dyn Module>>) -> Result<Vc<Vec<RcStr>>> {
148 let Some(ecmascript_asset) =
149 Vc::try_resolve_sidecast::<Box<dyn EcmascriptChunkPlaceable>>(module).await?
150 else {
151 return Ok(Default::default());
152 };
153
154 if let EcmascriptExports::EsmExports(exports) = &*ecmascript_asset.get_exports().await? {
155 let exports = &*exports.await?;
156 return Ok(Vc::cell(exports.exports.keys().cloned().collect()));
157 }
158
159 Ok(Vc::cell(Vec::new()))
160}