Skip to content

Commit 6cb97d6

Browse files
mischnicijjk
authored andcommitted
Turbopack: lazy require metadata and handle TLA (#91705)
A regression from #88487 1. Make metadata import lazy with `() => require()`, just like for the layout segments 2. Properly await the return value to better handle TLA modules This align with Webpack which does this: <img width="1535" height="509" alt="Bildschirmfoto 2026-03-20 um 11 58 00" src="https://github.com/user-attachments/assets/f2864c86-ccce-4884-8417-c8ae06c05f78" /> Closes PACK-6927 Closes #91700 Closes #91676
1 parent e6b101a commit 6cb97d6

File tree

8 files changed

+141
-50
lines changed

8 files changed

+141
-50
lines changed

crates/next-core/src/app_page_loader_tree.rs

Lines changed: 62 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ impl AppPageLoaderTreeBuilder {
189189
// when mixing ESM imports and requires).
190190
self.base.imports.push(
191191
format!(
192-
"const {identifier} = require(/*turbopackChunkingType: \
192+
"const {identifier} = () => require(/*turbopackChunkingType: \
193193
shared*/\"{inner_module_id}\");"
194194
)
195195
.into(),
@@ -208,7 +208,10 @@ impl AppPageLoaderTreeBuilder {
208208
.insert(inner_module_id.into(), module);
209209

210210
let s = " ";
211-
writeln!(self.loader_tree_code, "{s}{identifier}.default,")?;
211+
writeln!(
212+
self.loader_tree_code,
213+
"{s}async (props) => interopDefault(await {identifier}())(props),"
214+
)?;
212215
}
213216
}
214217
Ok(())
@@ -240,7 +243,7 @@ impl AppPageLoaderTreeBuilder {
240243
// requires).
241244
self.base.imports.push(
242245
format!(
243-
"const {identifier} = require(/*turbopackChunkingType: \
246+
"const {identifier} = () => require(/*turbopackChunkingType: \
244247
shared*/\"{inner_module_id}\");"
245248
)
246249
.into(),
@@ -255,8 +258,51 @@ impl AppPageLoaderTreeBuilder {
255258
.inner_assets
256259
.insert(inner_module_id.into(), module);
257260

261+
let alt = if let Some(alt_path) = alt_path {
262+
let identifier = magic_identifier::mangle(&format!("{name} alt text #{i}"));
263+
let inner_module_id = format!("METADATA_ALT_{i}");
264+
265+
// This should use the same importing mechanism as create_module_tuple_code, so that the
266+
// relative order of items is retained (which isn't the case when mixing ESM imports and
267+
// requires).
268+
self.base.imports.push(
269+
format!(
270+
"const {identifier} = () => require(/*turbopackChunkingType: \
271+
shared*/\"{inner_module_id}\");"
272+
)
273+
.into(),
274+
);
275+
276+
let module = self
277+
.base
278+
.process_source(Vc::upcast(TextContentFileSource::new(Vc::upcast(
279+
FileSource::new(alt_path),
280+
))))
281+
.to_resolved()
282+
.await?;
283+
284+
self.base
285+
.inner_assets
286+
.insert(inner_module_id.into(), module);
287+
288+
Some(identifier)
289+
} else {
290+
None
291+
};
292+
258293
let s = " ";
259-
writeln!(self.loader_tree_code, "{s}(async (props) => [{{")?;
294+
writeln!(self.loader_tree_code, "{s}(async (props) => {{")?;
295+
writeln!(
296+
self.loader_tree_code,
297+
"{s} const mod = interopDefault(await {identifier}());"
298+
)?;
299+
if let Some(alt) = &alt {
300+
writeln!(
301+
self.loader_tree_code,
302+
"{s} const alt = interopDefault(await {alt}());"
303+
)?;
304+
}
305+
writeln!(self.loader_tree_code, "{s} return [{{")?;
260306
let pathname_prefix = if let Some(base_path) = &self.base_path {
261307
format!("{base_path}/{app_page}")
262308
} else {
@@ -265,68 +311,37 @@ impl AppPageLoaderTreeBuilder {
265311
let metadata_route = &*get_metadata_route_name(item.clone().into()).await?;
266312
writeln!(
267313
self.loader_tree_code,
268-
"{s} url: fillMetadataSegment({}, await props.params, {}, true) + \
269-
`?${{{identifier}.default.src.split(\"/\").splice(-1)[0]}}`,",
314+
"{s} url: fillMetadataSegment({}, await props.params, {}, true) + \
315+
`?${{mod.src.split(\"/\").splice(-1)[0]}}`,",
270316
StringifyJs(&pathname_prefix),
271317
StringifyJs(metadata_route),
272318
)?;
273319

274320
let numeric_sizes = name == "twitter" || name == "openGraph";
275321
if numeric_sizes {
276-
writeln!(
277-
self.loader_tree_code,
278-
"{s} width: {identifier}.default.width,"
279-
)?;
280-
writeln!(
281-
self.loader_tree_code,
282-
"{s} height: {identifier}.default.height,"
283-
)?;
322+
writeln!(self.loader_tree_code, "{s} width: mod.width,")?;
323+
writeln!(self.loader_tree_code, "{s} height: mod.height,")?;
284324
} else {
285325
// For SVGs, skip sizes and use "any" to let it scale automatically based on viewport,
286326
// For the images doesn't provide the size properly, use "any" as well.
287327
// If the size is presented, use the actual size for the image.
288328
let sizes = if path.has_extension(".svg") {
289-
"any".to_string()
329+
"any"
290330
} else {
291-
format!("${{{identifier}.default.width}}x${{{identifier}.default.height}}")
331+
"${mod.width}x${mod.height}"
292332
};
293-
writeln!(self.loader_tree_code, "{s} sizes: `{sizes}`,")?;
333+
writeln!(self.loader_tree_code, "{s} sizes: `{sizes}`,")?;
294334
}
295335

296336
let content_type = get_content_type(path).await?;
297-
writeln!(self.loader_tree_code, "{s} type: `{content_type}`,")?;
298-
299-
if let Some(alt_path) = alt_path {
300-
let identifier = magic_identifier::mangle(&format!("{name} alt text #{i}"));
301-
let inner_module_id = format!("METADATA_ALT_{i}");
302-
303-
// This should use the same importing mechanism as create_module_tuple_code, so that the
304-
// relative order of items is retained (which isn't the case when mixing ESM imports and
305-
// requires).
306-
self.base.imports.push(
307-
format!(
308-
"const {identifier} = require(/*turbopackChunkingType: \
309-
shared*/\"{inner_module_id}\");"
310-
)
311-
.into(),
312-
);
313-
314-
let module = self
315-
.base
316-
.process_source(Vc::upcast(TextContentFileSource::new(Vc::upcast(
317-
FileSource::new(alt_path),
318-
))))
319-
.to_resolved()
320-
.await?;
321-
322-
self.base
323-
.inner_assets
324-
.insert(inner_module_id.into(), module);
337+
writeln!(self.loader_tree_code, "{s} type: `{content_type}`,")?;
325338

326-
writeln!(self.loader_tree_code, "{s} alt: {identifier}.default,")?;
339+
if alt.is_some() {
340+
writeln!(self.loader_tree_code, "{s} alt,")?;
327341
}
328342

329-
writeln!(self.loader_tree_code, "{s}}}]),")?;
343+
writeln!(self.loader_tree_code, "{s} }}];")?;
344+
writeln!(self.loader_tree_code, "{s}}}),")?;
330345

331346
Ok(())
332347
}

packages/next/src/lib/metadata/resolve-metadata.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ import {
4141
getComponentTypeModule,
4242
getLayoutOrPageModule,
4343
} from '../../server/lib/app-dir-module'
44-
import { interopDefault } from '../interop-default'
4544
import {
4645
resolveAlternates,
4746
resolveAppleWebApp,
@@ -572,11 +571,11 @@ async function collectStaticImagesFiles(
572571

573572
const iconPromises = metadata[type as 'icon' | 'apple'].map(
574573
async (imageModule: (p: any) => Promise<MetadataImageModule[]>) =>
575-
await interopDefault(imageModule)(props)
574+
await imageModule(props)
576575
)
577576

578577
return iconPromises?.length > 0
579-
? (await Promise.all(iconPromises))?.flat()
578+
? (await Promise.all(iconPromises)).flat()
580579
: undefined
581580
}
582581

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { ImageResponse } from 'next/og'
2+
import { getAllSlugs } from '../../data'
3+
4+
export default function og() {
5+
return new ImageResponse(
6+
(
7+
<div
8+
style={{
9+
width: '100%',
10+
height: '100%',
11+
display: 'flex',
12+
alignItems: 'center',
13+
justifyContent: 'center',
14+
fontSize: 88,
15+
background: 'lavender',
16+
}}
17+
>
18+
Posts: {getAllSlugs().join(', ')}
19+
</div>
20+
)
21+
)
22+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { getAllSlugs } from '../../data'
2+
3+
export function generateStaticParams() {
4+
return getAllSlugs().map((slug) => ({ slug }))
5+
}
6+
7+
export default async function Page({
8+
params,
9+
}: {
10+
params: Promise<{ slug: string }>
11+
}) {
12+
const { slug } = await params
13+
return <p>Post: {slug}</p>
14+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Async module: top-level await makes this an async module,
2+
// which causes any transitive importer to also become async.
3+
const data = await Promise.resolve({
4+
slugs: ['hello-world', 'another-post'],
5+
})
6+
7+
export function getAllSlugs(): string[] {
8+
return data.slugs
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function Layout({ children }) {
2+
return (
3+
<html>
4+
<head></head>
5+
<body>{children}</body>
6+
</html>
7+
)
8+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
3+
describe('app dir - metadata dynamic routes with async deps', () => {
4+
const { next } = nextTestSetup({
5+
files: __dirname,
6+
dependencies: {
7+
'@vercel/og': 'latest',
8+
},
9+
})
10+
11+
it('should render page with og:image meta tag when opengraph-image has async dependencies', async () => {
12+
const $ = await next.render$('/blog/hello-world')
13+
const ogImageUrl = $('meta[property="og:image"]').attr('content')
14+
expect(ogImageUrl).toContain('/blog/hello-world/opengraph-image')
15+
})
16+
17+
it('should serve the opengraph-image route as a valid image', async () => {
18+
const res = await next.fetch('/blog/hello-world/opengraph-image')
19+
expect(res.status).toBe(200)
20+
expect(res.headers.get('content-type')).toBe('image/png')
21+
})
22+
})
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/** @type {import('next').NextConfig} */
2+
module.exports = {}

0 commit comments

Comments
 (0)