Skip to main content

next_core/next_app/metadata/
image.rs

1//! (partial) Rust port of the `next-metadata-image-loader`
2//!
3//! See `next/src/build/webpack/loaders/next-metadata-image-loader`
4
5use 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}