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
44async fn dynamic_image_metadata_with_generator_source(
45 path: FileSystemPath,
46 ty: RcStr,
47 page: AppPage,
48 exported_fields_excluding_default: String,
49) -> Result<Vc<Box<dyn Source>>> {
50 let stem = path.file_stem();
51 let stem = stem.unwrap_or_default();
52 let ext = path.extension();
53
54 let hash_query = format!("?{:x}", hash_file_content(path.clone()).await?);
55
56 let use_numeric_sizes = ty == "twitter" || ty == "openGraph";
57 let sizes = if use_numeric_sizes {
58 "data.width = size.width; data.height = size.height;".to_string()
59 } else {
60 let sizes = if ext == "svg" {
61 "any"
62 } else {
63 "${size.width}x${size.height}"
64 };
65 format!("data.sizes = `{sizes}`;")
66 };
67
68 let code = formatdoc! {
69 r#"
70 import {{ {exported_fields_excluding_default} }} from {resource_path}
71 import {{ fillMetadataSegment }} from 'next/dist/lib/metadata/get-metadata-route'
72
73 const imageModule = {{ {exported_fields_excluding_default} }}
74
75 export default async function (props) {{
76 const {{ __metadata_id__: _, ...params }} = await props.params
77 const imageUrl = fillMetadataSegment({pathname_prefix}, params, {page_segment})
78
79 const {{ generateImageMetadata }} = imageModule
80
81 function getImageMetadata(imageMetadata, idParam) {{
82 const data = {{
83 alt: imageMetadata.alt,
84 type: imageMetadata.contentType || 'image/png',
85 url: imageUrl + (idParam ? ('/' + idParam) : '') + {hash_query},
86 }}
87 const {{ size }} = imageMetadata
88 if (size) {{
89 {sizes}
90 }}
91 return data
92 }}
93
94 const imageMetadataArray = await generateImageMetadata({{ params }})
95 return imageMetadataArray.map((imageMetadata, index) => {{
96 const idParam = imageMetadata.id + ''
97 return getImageMetadata(imageMetadata, idParam)
98 }})
99 }}
100 "#,
101 exported_fields_excluding_default = exported_fields_excluding_default,
102 resource_path = StringifyJs(&format!("./{stem}.{ext}")),
103 pathname_prefix = StringifyJs(&page.to_string()),
104 page_segment = StringifyJs(stem),
105 sizes = sizes,
106 hash_query = StringifyJs(&hash_query),
107 };
108
109 let file = File::from(code);
110 let source = VirtualSource::new(
111 path.parent().join(&format!("{stem}--metadata.js"))?,
112 AssetContent::file(file.into()),
113 );
114
115 Ok(Vc::upcast(source))
116}
117
118async fn dynamic_image_metadata_without_generator_source(
119 path: FileSystemPath,
120 ty: RcStr,
121 page: AppPage,
122 exported_fields_excluding_default: String,
123) -> Result<Vc<Box<dyn Source>>> {
124 let stem = path.file_stem();
125 let stem = stem.unwrap_or_default();
126 let ext = path.extension();
127
128 let hash_query = format!("?{:x}", hash_file_content(path.clone()).await?);
129
130 let use_numeric_sizes = ty == "twitter" || ty == "openGraph";
131 let sizes = if use_numeric_sizes {
132 "data.width = size.width; data.height = size.height;".to_string()
133 } else {
134 let sizes = if ext == "svg" {
135 "any"
136 } else {
137 "${size.width}x${size.height}"
138 };
139 format!("data.sizes = `{sizes}`;")
140 };
141
142 let code = formatdoc! {
143 r#"
144 import {{ {exported_fields_excluding_default} }} from {resource_path}
145 import {{ fillMetadataSegment }} from 'next/dist/lib/metadata/get-metadata-route'
146
147 const imageModule = {{ {exported_fields_excluding_default} }}
148
149 export default async function (props) {{
150 const {{ __metadata_id__: _, ...params }} = await props.params
151 const imageUrl = fillMetadataSegment({pathname_prefix}, params, {page_segment})
152
153 function getImageMetadata(imageMetadata, idParam) {{
154 const data = {{
155 alt: imageMetadata.alt,
156 type: imageMetadata.contentType || 'image/png',
157 url: imageUrl + (idParam ? ('/' + idParam) : '') + {hash_query},
158 }}
159 const {{ size }} = imageMetadata
160 if (size) {{
161 {sizes}
162 }}
163 return data
164 }}
165
166 return [getImageMetadata(imageModule, '')]
167 }}
168 "#,
169 exported_fields_excluding_default = exported_fields_excluding_default,
170 resource_path = StringifyJs(&format!("./{stem}.{ext}")),
171 pathname_prefix = StringifyJs(&page.to_string()),
172 page_segment = StringifyJs(stem),
173 sizes = sizes,
174 hash_query = StringifyJs(&hash_query),
175 };
176
177 let file = File::from(code);
178 let source = VirtualSource::new(
179 path.parent().join(&format!("{stem}--metadata.js"))?,
180 AssetContent::file(file.into()),
181 );
182
183 Ok(Vc::upcast(source))
184}
185
186#[turbo_tasks::function]
187pub async fn dynamic_image_metadata_source(
188 asset_context: Vc<Box<dyn AssetContext>>,
189 path: FileSystemPath,
190 ty: RcStr,
191 page: AppPage,
192) -> Result<Vc<Box<dyn Source>>> {
193 let source = Vc::upcast(FileSource::new(path.clone()));
194 let module = asset_context
195 .process(
196 source,
197 ReferenceType::EcmaScriptModules(EcmaScriptModulesReferenceSubType::Undefined),
198 )
199 .module();
200 let exports = &*collect_direct_exports(module).await?;
201 let exported_fields_excluding_default = exports
202 .iter()
203 .filter(|e| *e != "default")
204 .cloned()
205 .collect::<Vec<_>>()
206 .join(", ");
207
208 let has_generate_image_metadata = exports.contains(&"generateImageMetadata".into());
209
210 if has_generate_image_metadata {
211 dynamic_image_metadata_with_generator_source(
212 path,
213 ty,
214 page,
215 exported_fields_excluding_default,
216 )
217 .await
218 } else {
219 dynamic_image_metadata_without_generator_source(
220 path,
221 ty,
222 page,
223 exported_fields_excluding_default,
224 )
225 .await
226 }
227}
228
229#[turbo_tasks::function]
230async fn collect_direct_exports(module: Vc<Box<dyn Module>>) -> Result<Vc<Vec<RcStr>>> {
231 let Some(ecmascript_asset) =
232 Vc::try_resolve_sidecast::<Box<dyn EcmascriptChunkPlaceable>>(module).await?
233 else {
234 return Ok(Default::default());
235 };
236
237 if let EcmascriptExports::EsmExports(exports) = &*ecmascript_asset.get_exports().await? {
238 let exports = &*exports.await?;
239 return Ok(Vc::cell(exports.exports.keys().cloned().collect()));
240 }
241
242 Ok(Vc::cell(Vec::new()))
243}