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