mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
https://github.com/vercel/next.js/pull/95550 wasn't enough: - https://github.com/vercel/next.js/actions/runs/28907225463/job/85769093204 - https://github.com/vercel/next.js/actions/runs/28907225463/job/85769093252 Because `VERCEL_IMMUTABLE_STATIC_FILES_ENABLED` might be set, but it still only works when using adapters, so it broke in the non-adapter deploy test jobs. Also prepare for when we switch the default of `NEXT_ENABLE_ADAPTER` by setting it explicitly to `0` for when we want to test the old CLI builder.
258 lines
9.3 KiB
TypeScript
258 lines
9.3 KiB
TypeScript
import { nextTestSetup } from 'e2e-utils'
|
|
import * as cheerio from 'cheerio'
|
|
import { getCacheHeader, retry } from 'next-test-utils'
|
|
import { computeCacheBustingSearchParam } from 'next/dist/shared/lib/router/utils/cache-busting-search-param'
|
|
|
|
const isAdapterTest = process.env.NEXT_ENABLE_ADAPTER === '1'
|
|
|
|
describe('middleware-static-rewrite', () => {
|
|
const { next, isNextDeploy, isNextDev } = nextTestSetup({
|
|
files: __dirname,
|
|
// The latest changes to support this behavior on deployed infra are available in the adapter,
|
|
// and are not being backported to the CLI
|
|
skipDeployment: !isAdapterTest,
|
|
})
|
|
|
|
if (isNextDev) {
|
|
it.skip('skipping dev test', () => {})
|
|
return
|
|
}
|
|
|
|
if (process.env.__NEXT_CACHE_COMPONENTS === 'true') {
|
|
// Here we're validating that the correct fallback shell was used for
|
|
// rendering.
|
|
it('should use the correct fallback route', async () => {
|
|
// First try to load a page that'll use the base fallback route with the
|
|
// `/[first]/[second]/[third]` fallback.
|
|
let $ = await next.render$('/first/second/third')
|
|
|
|
expect($('[data-slug]').data('slug')).toBe('first/second/third')
|
|
|
|
// Get the sentinel value that was generated at build time or runtime.
|
|
expect($('[data-layout="/"]').data('sentinel')).toBe('buildtime')
|
|
expect($('[data-layout="/[first]"]').data('sentinel')).toBe('buildtime')
|
|
expect($('[data-layout="/[first]/[second]"]').data('sentinel')).toBe(
|
|
'buildtime'
|
|
)
|
|
expect(
|
|
$('[data-layout="/[first]/[second]/[third]"]').data('sentinel')
|
|
).toBe('buildtime')
|
|
|
|
// Then we try to load a page that'll use the `/first/second/[third]`
|
|
// fallback.
|
|
$ = await next.render$('/first/second/not-third')
|
|
|
|
expect($('[data-slug]').data('slug')).toBe('first/second/not-third')
|
|
|
|
expect($('[data-layout="/"]').data('sentinel')).toBe('buildtime')
|
|
expect($('[data-layout="/[first]"]').data('sentinel')).toBe('buildtime')
|
|
expect($('[data-layout="/[first]/[second]"]').data('sentinel')).toBe(
|
|
'buildtime'
|
|
)
|
|
expect(
|
|
$('[data-layout="/[first]/[second]/[third]"]').data('sentinel')
|
|
).toBe('runtime')
|
|
|
|
// Then we try to load a page that'll use the `/first/[second]/[third]`
|
|
$ = await next.render$('/first/not-second/not-third')
|
|
|
|
expect($('[data-slug]').data('slug')).toBe('first/not-second/not-third')
|
|
|
|
expect($('[data-layout="/"]').data('sentinel')).toBe('buildtime')
|
|
expect($('[data-layout="/[first]"]').data('sentinel')).toBe('buildtime')
|
|
expect($('[data-layout="/[first]/[second]"]').data('sentinel')).toBe(
|
|
'runtime'
|
|
)
|
|
expect(
|
|
$('[data-layout="/[first]/[second]/[third]"]').data('sentinel')
|
|
).toBe('runtime')
|
|
|
|
// Then we try to load a page that'll use the `/[first]/[second]/[third]`
|
|
$ = await next.render$('/not-first/not-second/not-third')
|
|
|
|
expect($('[data-slug]').data('slug')).toBe(
|
|
'not-first/not-second/not-third'
|
|
)
|
|
|
|
expect($('[data-layout="/"]').data('sentinel')).toBe('buildtime')
|
|
expect($('[data-layout="/[first]"]').data('sentinel')).toBe('runtime')
|
|
expect($('[data-layout="/[first]/[second]"]').data('sentinel')).toBe(
|
|
'runtime'
|
|
)
|
|
expect(
|
|
$('[data-layout="/[first]/[second]/[third]"]').data('sentinel')
|
|
).toBe('runtime')
|
|
})
|
|
|
|
it('should handle middleware rewrites as well', async () => {
|
|
let res = await next.fetch('/not-broken')
|
|
|
|
expect(res.status).toBe(200)
|
|
|
|
if (isNextDeploy) {
|
|
// We produced a partial fallback shell for rewrite/[slug], so we shouldn't see a cache HIT.
|
|
expect(getCacheHeader(res)).toMatch(/MISS|PRERENDER/)
|
|
} else {
|
|
expect(res.headers.get('x-nextjs-cache')).toBe(null)
|
|
}
|
|
|
|
let html = await res.text()
|
|
let $ = cheerio.load(html)
|
|
|
|
expect($('[data-layout="/"]').data('sentinel')).toBe('buildtime')
|
|
expect($('[data-layout="/rewrite"]').data('sentinel')).toBe('buildtime')
|
|
expect($('[data-layout="/rewrite/[slug]"]').data('sentinel')).toBe(
|
|
'runtime'
|
|
)
|
|
|
|
await retry(async () => {
|
|
res = await next.fetch('/not-broken')
|
|
|
|
expect(res.status).toBe(200)
|
|
if (isNextDeploy) {
|
|
expect(getCacheHeader(res)).toBe('HIT')
|
|
} else {
|
|
expect(res.headers.get('x-nextjs-cache')).toBe('HIT')
|
|
}
|
|
|
|
html = await res.text()
|
|
$ = cheerio.load(html)
|
|
|
|
expect($('[data-rewrite-slug]').data('rewrite-slug')).toBe('not-broken')
|
|
|
|
// With partial fallback upgrading, the cached entry upgrades from a
|
|
// fallback shell to a full route render.
|
|
// These assertions are part of the outer retry block because the fallback->route shell upgrade
|
|
// happens in the background. It's possible to see a cache HIT for the fallback shell before it
|
|
// switches to the full route shell.
|
|
expect($('[data-layout="/"]').data('sentinel')).toBe('runtime')
|
|
expect($('[data-layout="/rewrite"]').data('sentinel')).toBe('runtime')
|
|
expect($('[data-layout="/rewrite/[slug]"]').data('sentinel')).toBe(
|
|
'runtime'
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should revalidate the overview page without replacing it with a 404', async () => {
|
|
const url = new URL('/my-team', 'http://localhost')
|
|
const rsc = await computeCacheBustingSearchParam(
|
|
'1',
|
|
'/_head',
|
|
undefined,
|
|
undefined
|
|
)
|
|
|
|
url.searchParams.set('_rsc', rsc)
|
|
|
|
let res = await next.fetch(url.pathname + url.search, {
|
|
headers: {
|
|
Cookie: 'overview-param=grid',
|
|
RSC: '1',
|
|
'Next-Router-Prefetch': '1',
|
|
'Next-Router-Segment-Prefetch': '/_head',
|
|
},
|
|
})
|
|
|
|
// A 404 here represents a routing issue that was resolved by an upstream
|
|
// PR in the builder: https://github.com/vercel/vercel/pull/13927
|
|
expect(res.status).toBe(200)
|
|
|
|
// Now, let's verify that we both got rewritten to the correct page, and
|
|
// that we're being served the prerender shell.
|
|
expect(res.headers.get('x-nextjs-rewritten-path')).toBe(
|
|
'/my-team/~/overview/grid'
|
|
)
|
|
expect(res.headers.get('x-nextjs-postponed')).toBe('2')
|
|
expect(res.headers.get('x-nextjs-prerender')).toBe('1')
|
|
|
|
// Grab the RSC content.
|
|
const rsc1 = await res.text()
|
|
|
|
// Grab the title which includes the random number.
|
|
const title1 = rsc1.match(/Grid Page (\d+\.\d+)/)?.[1]
|
|
expect(title1).toBeDefined()
|
|
|
|
// Now, let's trigger a revalidation for the page.
|
|
res = await next.fetch(
|
|
'/api?path=/my-team&path=/my-team/~/overview/grid&path=/[first]/~/overview/grid'
|
|
)
|
|
|
|
// Now, let's keep polling the prefetch until it's revalidated.
|
|
let rsc2: string
|
|
await retry(async () => {
|
|
res = await next.fetch(url.pathname + url.search, {
|
|
headers: {
|
|
Cookie: 'overview-param=grid',
|
|
RSC: '1',
|
|
'Next-Router-Prefetch': '1',
|
|
'Next-Router-Segment-Prefetch': '/_head',
|
|
},
|
|
})
|
|
|
|
// A 404 here represents a routing issue that was resolved by an upstream
|
|
// PR in the builder: https://github.com/vercel/vercel/pull/13927
|
|
expect(res.status).toBe(200)
|
|
|
|
// We're expecting that the title has changed, so let's compare that the
|
|
// rsc payload is different.
|
|
rsc2 = await res.text()
|
|
expect(rsc1).not.toBe(rsc2)
|
|
})
|
|
|
|
// Now that the revalidation has been completed, let's also verify that
|
|
// it revalidated correctly.
|
|
expect(res.headers.get('x-nextjs-postponed')).toBe('2')
|
|
expect(res.headers.get('x-nextjs-rewritten-path')).toBe(
|
|
'/my-team/~/overview/grid'
|
|
)
|
|
|
|
// We expect that the only difference between the two RSC contents is the
|
|
// title.
|
|
const title2 = rsc2.match(/Grid Page (\d+\.\d+)/)?.[1]
|
|
expect(title2).toBeDefined()
|
|
expect(title2).not.toBe(title1)
|
|
|
|
// Let's compare the RSC contents, with the titles removed.
|
|
const cleaned1 = rsc1.replace(/Grid Page (\d+\.\d+)/, 'Grid Page')
|
|
const cleaned2 = rsc2.replace(/Grid Page (\d+\.\d+)/, 'Grid Page')
|
|
expect(cleaned1).toBe(cleaned2)
|
|
})
|
|
} else {
|
|
// Here we're validating that there is a static page generated for the
|
|
// rewritten path.
|
|
it('should eventually result in a cache hit', async () => {
|
|
let res = await next.fetch('/not-broken')
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(getCacheHeader(res)).toMatch(/MISS|HIT|PRERENDER/)
|
|
|
|
let html = await res.text()
|
|
let $ = cheerio.load(html)
|
|
|
|
expect($('[data-layout="/"]').data('sentinel')).toBe('runtime')
|
|
expect($('[data-layout="/rewrite"]').data('sentinel')).toBe('runtime')
|
|
expect($('[data-layout="/rewrite/[slug]"]').data('sentinel')).toBe(
|
|
'runtime'
|
|
)
|
|
|
|
await retry(async () => {
|
|
res = await next.fetch('/not-broken')
|
|
|
|
expect(res.status).toBe(200)
|
|
expect(getCacheHeader(res)).toBe('HIT')
|
|
})
|
|
|
|
html = await res.text()
|
|
$ = cheerio.load(html)
|
|
|
|
expect($('[data-rewrite-slug]').data('rewrite-slug')).toBe('not-broken')
|
|
|
|
expect($('[data-layout="/"]').data('sentinel')).toBe('runtime')
|
|
expect($('[data-layout="/rewrite"]').data('sentinel')).toBe('runtime')
|
|
expect($('[data-layout="/rewrite/[slug]"]').data('sentinel')).toBe(
|
|
'runtime'
|
|
)
|
|
})
|
|
}
|
|
})
|