mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
Extracted from https://github.com/vercel/next.js/pull/95468 and arguably the main fix there. We need to add code to wait for top-level awaits in two cases: - If a metadata module (like `opengraph-image.tsx`) is slow to evaluate, cache warming wouldn't "wait" for it, and then the actual build would fail with `Unexpected cache miss after cache warming phase`. This is fixed with placing `instrumentModuleGetter` around them. - If the evaluation is fast but still async (e.g. `await readFile` on a large font), the build passes but the segment collector reads the module too early, doesn't see `generateStaticParams`, and so the image becomes dynamic instead of prerendered. The fix is `await ensureUserland` before looking at exports. Remaining callsites are wrapped so we don't add more bugs like this, but it's not strictly required now because they are already being awaited elsewhere. I've done this for consistency. New tests fail before the fix.
222 lines
6.7 KiB
TypeScript
222 lines
6.7 KiB
TypeScript
import { nextTestSetup } from 'e2e-utils'
|
|
|
|
describe('use-cache-metadata-route-handler', () => {
|
|
const { next, isNextDev, isNextStart } = nextTestSetup({
|
|
files: __dirname,
|
|
})
|
|
|
|
it('should generate an opengraph image with a metadata route handler that uses "use cache"', async () => {
|
|
const res = await next.fetch('/opengraph-image')
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('content-type')).toBe('image/png')
|
|
|
|
if (isNextStart) {
|
|
const [buildStatus] = next.cliOutput.match(/. \/opengraph-image/)
|
|
|
|
expect(buildStatus).toBe('○ /opengraph-image')
|
|
}
|
|
})
|
|
|
|
it('should generate an opengraph image with a custom (local) font', async () => {
|
|
const res = await next.fetch('/custom-font/opengraph-image')
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('content-type')).toBe('image/png')
|
|
|
|
if (isNextStart) {
|
|
const [buildStatus] = next.cliOutput.match(
|
|
/. \/custom-font\/opengraph-image/
|
|
)
|
|
|
|
expect(buildStatus).toBe('○ /custom-font/opengraph-image')
|
|
}
|
|
})
|
|
|
|
it('should prerender a page that shares a segment with an opengraph image that uses a custom font', async () => {
|
|
const res = await next.fetch('/first-post')
|
|
expect(res.status).toBe(200)
|
|
expect(await res.text()).toContain('First Post')
|
|
|
|
const imageRes = await next.fetch('/first-post/opengraph-image')
|
|
expect(imageRes.status).toBe(200)
|
|
expect(imageRes.headers.get('content-type')).toBe('image/png')
|
|
|
|
expect(next.cliOutput).not.toContain(
|
|
'Unexpected cache miss after cache warming phase'
|
|
)
|
|
|
|
if (isNextStart) {
|
|
// The image route uses generateStaticParams, so the build is expected
|
|
// to prerender it for each param.
|
|
expect(next.cliOutput).toMatch(/● \/first-post\/opengraph-image/)
|
|
expect(next.cliOutput).toMatch(/● \/second-post\/opengraph-image/)
|
|
}
|
|
})
|
|
|
|
it('should generate an icon image with a metadata route handler that uses "use cache"', async () => {
|
|
const res = await next.fetch('/icon')
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('content-type')).toBe('image/png')
|
|
|
|
if (isNextStart) {
|
|
const [buildStatus] = next.cliOutput.match(/. \/icon/)
|
|
|
|
expect(buildStatus).toBe('○ /icon')
|
|
}
|
|
})
|
|
|
|
it('should statically prerender an image whose component uses "use cache" directly', async () => {
|
|
const res = await next.fetch('/apple-icon')
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('content-type')).toBe('image/png')
|
|
|
|
if (isNextStart) {
|
|
const [buildStatus] = next.cliOutput.match(/. \/apple-icon/)
|
|
|
|
expect(buildStatus).toBe('○ /apple-icon')
|
|
}
|
|
})
|
|
|
|
it('should treat a twitter image that reads request data as dynamic', async () => {
|
|
const res = await next.fetch('/twitter-image')
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('content-type')).toBe('image/png')
|
|
|
|
if (isNextStart) {
|
|
const [buildStatus] = next.cliOutput.match(/. \/twitter-image/)
|
|
|
|
expect(buildStatus).toBe('ƒ /twitter-image')
|
|
}
|
|
})
|
|
|
|
it('should generate sitemaps with a metadata route handler that uses "use cache"', async () => {
|
|
const res = await next.fetch('/sitemap.xml')
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('content-type')).toBe('application/xml')
|
|
|
|
const body = await res.text()
|
|
|
|
if (isNextDev) {
|
|
expect(body).toMatchInlineSnapshot(`
|
|
"<?xml version="1.0" encoding="UTF-8"?>
|
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
<url>
|
|
<loc>https://acme.com?sentinel=runtime</loc>
|
|
</url>
|
|
</urlset>
|
|
"
|
|
`)
|
|
} else {
|
|
expect(body).toMatchInlineSnapshot(`
|
|
"<?xml version="1.0" encoding="UTF-8"?>
|
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
<url>
|
|
<loc>https://acme.com?sentinel=buildtime</loc>
|
|
</url>
|
|
</urlset>
|
|
"
|
|
`)
|
|
}
|
|
})
|
|
|
|
it('should generate multiple sitemaps with a metadata route handler that uses "use cache"', async () => {
|
|
const res = await next.fetch('/products/sitemap/1.xml')
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('content-type')).toBe('application/xml')
|
|
|
|
const body = await res.text()
|
|
|
|
if (isNextDev) {
|
|
expect(body).toMatchInlineSnapshot(`
|
|
"<?xml version="1.0" encoding="UTF-8"?>
|
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
<url>
|
|
<loc>https://acme.com/1?sentinel=runtime</loc>
|
|
</url>
|
|
</urlset>
|
|
"
|
|
`)
|
|
} else {
|
|
expect(body).toMatchInlineSnapshot(`
|
|
"<?xml version="1.0" encoding="UTF-8"?>
|
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
<url>
|
|
<loc>https://acme.com/1?sentinel=buildtime</loc>
|
|
</url>
|
|
</urlset>
|
|
"
|
|
`)
|
|
}
|
|
})
|
|
|
|
it('should generate robots.txt with a metadata route handler that uses "use cache"', async () => {
|
|
const res = await next.fetch('/robots.txt')
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('content-type')).toContain('text/plain')
|
|
|
|
const body = await res.text()
|
|
|
|
if (isNextDev) {
|
|
expect(body).toMatchInlineSnapshot(`
|
|
"User-Agent: *
|
|
Allow: /runtime
|
|
|
|
"
|
|
`)
|
|
} else {
|
|
expect(body).toMatchInlineSnapshot(`
|
|
"User-Agent: *
|
|
Allow: /buildtime
|
|
|
|
"
|
|
`)
|
|
}
|
|
})
|
|
|
|
it('should generate manifest.json with a metadata route handler that uses "use cache"', async () => {
|
|
const res = await next.fetch('/manifest.webmanifest')
|
|
expect(res.status).toBe(200)
|
|
expect(res.headers.get('content-type')).toContain(
|
|
'application/manifest+json'
|
|
)
|
|
|
|
const body = await res.json()
|
|
|
|
if (isNextDev) {
|
|
expect(body).toEqual({ name: 'runtime' })
|
|
} else {
|
|
expect(body).toEqual({ name: 'buildtime' })
|
|
}
|
|
})
|
|
|
|
if (isNextStart) {
|
|
it('should include the client reference manifest in the route.js.nft.json files of dynamic metadata routes', async () => {
|
|
for (const filename of [
|
|
'icon',
|
|
'manifest.webmanifest',
|
|
'opengraph-image',
|
|
'products/sitemap/[__metadata_id__]',
|
|
'robots.txt',
|
|
'sitemap.xml',
|
|
]) {
|
|
const { files } = await next.readJSON(
|
|
`/.next/server/app/${filename}/route.js.nft.json`
|
|
)
|
|
|
|
expect(
|
|
files.find((e) => e.endsWith('route_client-reference-manifest.js'))
|
|
).toBeString()
|
|
}
|
|
})
|
|
|
|
it('should not include the client reference manifest in the route.js.nft.json files of static metadata routes', async () => {
|
|
const { files } = await next.readJSON(
|
|
'/.next/server/app/favicon.ico/route.js.nft.json'
|
|
)
|
|
|
|
expect(
|
|
files.find((e) => e.endsWith('route_client-reference-manifest.js'))
|
|
).toBeUndefined()
|
|
})
|
|
}
|
|
})
|