Skip to content

Commit 3ed9f4b

Browse files
authored
Respect reexports from metadata API routes (#70508)
1 parent 154dd24 commit 3ed9f4b

File tree

7 files changed

+141
-17
lines changed

7 files changed

+141
-17
lines changed

packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts

+34-17
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,27 @@ function errorOnBadHandler(resourcePath: string) {
1414
`
1515
}
1616

17+
/* re-export the userland route configs */
18+
async function createReExportsCode(
19+
resourcePath: string,
20+
loaderContext: webpack.LoaderContext<any>
21+
) {
22+
const exportNames = await getLoaderModuleNamedExports(
23+
resourcePath,
24+
loaderContext
25+
)
26+
// Re-export configs but avoid conflicted exports
27+
const reExportNames = exportNames.filter(
28+
(name) => name !== 'default' && name !== 'generateSitemaps'
29+
)
30+
31+
return reExportNames.length > 0
32+
? `export { ${reExportNames.join(', ')} } from ${JSON.stringify(
33+
resourcePath
34+
)}\n`
35+
: ''
36+
}
37+
1738
const cacheHeader = {
1839
none: 'no-cache, no-store',
1940
longCache: 'public, immutable, no-transform, max-age=31536000',
@@ -85,7 +106,10 @@ export const dynamic = 'force-static'
85106
return code
86107
}
87108

88-
function getDynamicTextRouteCode(resourcePath: string) {
109+
async function getDynamicTextRouteCode(
110+
resourcePath: string,
111+
loaderContext: webpack.LoaderContext<any>
112+
) {
89113
return `\
90114
/* dynamic asset route */
91115
import { NextResponse } from 'next/server'
@@ -96,6 +120,7 @@ const contentType = ${JSON.stringify(getContentType(resourcePath))}
96120
const fileType = ${JSON.stringify(getFilenameAndExtension(resourcePath).name)}
97121
98122
${errorOnBadHandler(resourcePath)}
123+
${await createReExportsCode(resourcePath, loaderContext)}
99124
100125
export async function GET() {
101126
const data = await handler()
@@ -112,7 +137,10 @@ export async function GET() {
112137
}
113138

114139
// <metadata-image>/[id]/route.js
115-
function getDynamicImageRouteCode(resourcePath: string) {
140+
async function getDynamicImageRouteCode(
141+
resourcePath: string,
142+
loaderContext: webpack.LoaderContext<any>
143+
) {
116144
return `\
117145
/* dynamic image route */
118146
import { NextResponse } from 'next/server'
@@ -124,6 +152,7 @@ const handler = imageModule.default
124152
const generateImageMetadata = imageModule.generateImageMetadata
125153
126154
${errorOnBadHandler(resourcePath)}
155+
${await createReExportsCode(resourcePath, loaderContext)}
127156
128157
export async function GET(_, ctx) {
129158
const { __metadata_id__, ...params } = ctx.params || {}
@@ -162,10 +191,6 @@ async function getDynamicSitemapRouteCode(
162191
resourcePath,
163192
loaderContext
164193
)
165-
// Re-export configs but avoid conflicted exports
166-
const reExportNames = exportNames.filter(
167-
(name) => name !== 'default' && name !== 'generateSitemaps'
168-
)
169194

170195
const hasGenerateSitemaps = exportNames.includes('generateSitemaps')
171196

@@ -195,15 +220,7 @@ const contentType = ${JSON.stringify(getContentType(resourcePath))}
195220
const fileType = ${JSON.stringify(getFilenameAndExtension(resourcePath).name)}
196221
197222
${errorOnBadHandler(resourcePath)}
198-
199-
${'' /* re-export the userland route configs */}
200-
${
201-
reExportNames.length > 0
202-
? `export { ${reExportNames.join(', ')} } from ${JSON.stringify(
203-
resourcePath
204-
)}\n`
205-
: ''
206-
}
223+
${await createReExportsCode(resourcePath, loaderContext)}
207224
208225
export async function GET(_, ctx) {
209226
const { __metadata_id__: id, ...params } = ctx.params || {}
@@ -253,11 +270,11 @@ const nextMetadataRouterLoader: webpack.LoaderDefinitionFunction<MetadataRouteLo
253270
let code = ''
254271
if (isDynamicRouteExtension === '1') {
255272
if (fileBaseName === 'robots' || fileBaseName === 'manifest') {
256-
code = getDynamicTextRouteCode(filePath)
273+
code = await getDynamicTextRouteCode(filePath, this)
257274
} else if (fileBaseName === 'sitemap') {
258275
code = await getDynamicSitemapRouteCode(filePath, this)
259276
} else {
260-
code = getDynamicImageRouteCode(filePath)
277+
code = await getDynamicImageRouteCode(filePath, this)
261278
}
262279
} else {
263280
code = await getStaticAssetRouteCode(filePath, fileBaseName)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { MetadataRoute } from 'next'
2+
3+
export default function manifest(): MetadataRoute.Manifest {
4+
return {
5+
name: 'Next.js App',
6+
short_name: 'Next.js App',
7+
description: 'Next.js App',
8+
start_url: '/',
9+
display: 'standalone',
10+
background_color: '#fff',
11+
theme_color: '#fff',
12+
icons: [
13+
{
14+
src: '/favicon.ico',
15+
sizes: 'any',
16+
type: 'image/x-icon',
17+
},
18+
],
19+
}
20+
}
21+
22+
export const revalidate = 5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ImageResponse } from 'next/og'
2+
3+
/* without generateImageMetadata */
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: 128,
15+
background: 'lavender',
16+
}}
17+
>
18+
Open Graph
19+
</div>
20+
)
21+
)
22+
}
23+
24+
export const revalidate = 5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { MetadataRoute } from 'next'
2+
3+
export default function robots(): MetadataRoute.Robots {
4+
return {
5+
rules: {
6+
userAgent: '*',
7+
allow: '/',
8+
disallow: '/private/',
9+
},
10+
sitemap: 'https://acme.com/sitemap.xml',
11+
}
12+
}
13+
14+
export const revalidate = 5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export default function sitemap() {
2+
return [
3+
{
4+
url: 'https://acme.com',
5+
lastModified: new Date(),
6+
changeFrequency: 'yearly',
7+
priority: 1,
8+
},
9+
]
10+
}
11+
12+
export const revalidate = 5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
3+
describe('app-dir - metadata-revalidate', () => {
4+
const { next } = nextTestSetup({
5+
files: __dirname,
6+
})
7+
8+
it('should contain the routes in prerender manifest', async () => {
9+
const manifestContent = await next.readFile('.next/prerender-manifest.json')
10+
const prerenderManifest = JSON.parse(manifestContent)
11+
12+
expect(
13+
prerenderManifest.routes['/revalidate/og/opengraph-image']
14+
.initialRevalidateSeconds
15+
).toBe(5)
16+
expect(
17+
prerenderManifest.routes['/manifest.webmanifest'].initialRevalidateSeconds
18+
).toBe(5)
19+
expect(
20+
prerenderManifest.routes['/robots.txt'].initialRevalidateSeconds
21+
).toBe(5)
22+
expect(
23+
prerenderManifest.routes['/sitemap.xml'].initialRevalidateSeconds
24+
).toBe(5)
25+
})
26+
})

test/turbopack-build-tests-manifest.json

+9
Original file line numberDiff line numberDiff line change
@@ -16302,6 +16302,15 @@
1630216302
"flakey": [],
1630316303
"runtimeError": false
1630416304
},
16305+
"test/production/app-dir/metadata-revalidate/metadata-revalidate.test.ts": {
16306+
"passed": [],
16307+
"failed": [
16308+
"app-dir - metadata-revalidate should contain the routes in prerender manifest"
16309+
],
16310+
"pending": [],
16311+
"flakey": [],
16312+
"runtimeError": false
16313+
},
1630516314
"test/production/middleware-typescript/test/index.test.ts": {
1630616315
"passed": ["middleware-typescript should have built and started"],
1630716316
"failed": [],

0 commit comments

Comments
 (0)