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