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,
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}