Files
JJ KasperandGitHub a6b36200ff Wire cache handlers in edge paths and add e2e regression coverage (#91236)
### What?

This PR wires custom `cacheHandler` / `cacheHandlers` through the
remaining edge entrypoints so edge app pages, edge app routes, edge
pages SSR, middleware, and edge API routes can all see the configured
handlers.

It also adds an e2e regression suite covering:
- pages-router ISR revalidation with a custom incremental cache handler
- app-router cache handlers with cache components enabled
- edge app page + edge app route wiring when cache components are
disabled

### Why?

We already support custom cache handlers in non-edge paths, but several
edge code paths were not forwarding that configuration into the
generated entrypoints/runtime wrappers. That meant custom cache handlers
could be skipped in edge rendering and edge route execution, and we did
not have regression coverage around those cases.

### How?

- pass `cacheHandler` / `cacheHandlers` through the JS build entry
plumbing for edge server entries
- inject cache handler imports/registration into the webpack edge
templates and loaders
- mirror the same wiring in the Turbopack/Rust entry generation for edge
app pages, app routes, pages SSR, and middleware
- update the edge route wrapper to initialize and register cache
handlers before invoking the route module
- extend `next-taskless` template expansion with raw injection support
so the generated edge templates can add imports and registration code
- add `test/e2e/cache-handlers-upstream-wiring` fixtures to cover
pages/app, edge/non-edge, and revalidation behavior
2026-03-13 17:25:59 +00:00

138 lines
4.3 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
import { join } from 'path'
describe('cache-handlers-upstream-wiring', () => {
describe('pages router non-edge', () => {
const { next, skipped, isNextDev } = nextTestSetup({
files: join(__dirname, 'fixtures/pages-router-non-edge'),
skipDeployment: true,
})
if (skipped) {
return
}
let outputIndex = 0
beforeEach(() => {
outputIndex = next.cliOutput.length
})
it('wires res.revalidate() through the configured custom cacheHandler', async () => {
const initialHtml = await next.render$('/isr')
const initialValue = initialHtml('#now').text()
outputIndex = next.cliOutput.length
const revalidateResponse = await next.fetch('/api/revalidate')
expect(revalidateResponse.status).toBe(200)
expect(await revalidateResponse.json()).toEqual({ revalidated: true })
if (!isNextDev) {
await retry(async () => {
const htmlAfterRevalidate = await next.render$('/isr')
const valueAfterRevalidate = htmlAfterRevalidate('#now').text()
expect(valueAfterRevalidate).not.toBe(initialValue)
})
}
await next.render$('/isr')
await retry(async () => {
const output = next.cliOutput.slice(outputIndex)
const constructorLogs =
output.match(/WiringPagesIncrementalCacheHandler::constructor/g) ?? []
const sawRevalidateTag = output.includes(
'WiringPagesIncrementalCacheHandler::revalidateTag'
)
expect(sawRevalidateTag || constructorLogs.length > 0).toBe(true)
})
})
})
describe('cacheComponents enabled, non-edge app router', () => {
const { next, skipped, isNextDev } = nextTestSetup({
files: join(__dirname, 'fixtures/non-edge-cache-components'),
skipDeployment: true,
})
if (skipped) {
return
}
let outputIndex = 0
beforeEach(() => {
outputIndex = next.cliOutput.length
})
it('uses configured cacheHandlers for custom app-router cache kind', async () => {
const pageResponse = await next.fetch('/')
expect(pageResponse.status).toBe(200)
await retry(async () => {
const output = isNextDev
? next.cliOutput.slice(outputIndex)
: next.cliOutput
expect(output).toContain('WiringModernCacheHandler::set')
expect(output).toContain('WiringModernCacheHandler::get')
})
})
it('wires revalidateTag and revalidatePath to the custom cache handler', async () => {
const seedResponse = await next.fetch('/revalidate-target')
expect(seedResponse.status).toBe(200)
const browser = await next.browser('/revalidate-actions')
await browser.elementById('revalidate-tag').click()
await browser.elementById('revalidate-path').click()
await retry(async () => {
const output = next.cliOutput.slice(outputIndex)
const updateTagLogs =
output.match(/WiringModernCacheHandler::updateTags/g) ?? []
expect(updateTagLogs.length).toBeGreaterThanOrEqual(2)
expect(output).toContain('custom-tag')
})
})
})
;(process.env.__NEXT_CACHE_COMPONENTS ? describe.skip : describe)(
'cacheComponents disabled, edge app router',
() => {
const { next, skipped } = nextTestSetup({
files: join(__dirname, 'fixtures/edge-without-cache-components'),
skipDeployment: true,
})
if (skipped) {
return
}
let outputIndex = 0
beforeEach(() => {
outputIndex = next.cliOutput.length
})
it('uses configured cacheHandler for edge app page and edge app route', async () => {
const edgePageResponse = await next.fetch('/edge-page')
expect(edgePageResponse.status).toBe(200)
const edgeRouteResponse = await next.fetch('/api/edge-route')
expect(edgeRouteResponse.status).toBe(200)
expect(await edgeRouteResponse.json()).toEqual({ ok: true })
await retry(async () => {
const output = next.cliOutput.slice(outputIndex)
const constructorLogs =
output.match(/WiringIncrementalCacheHandler::constructor/g) ?? []
expect(constructorLogs.length).toBeGreaterThanOrEqual(2)
})
})
}
)
})