mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
codex/ledgers-react-sync
128 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dee811ff1c |
test: enable verified caching deploy tests (#98523)
## Summary
Enable the same 24 previously selected deployment-test scopes across 21
caching test files, now in a stack rooted on canary. Remove 24
`@force-gate !deploy` directives and their associated TODO comments,
which are already present on canary. Other mode, bundler, middleware,
and Cache Components exclusions remain in place.
This preserves the selection with passing evidence from the previous
deployment runs. No additional candidate scopes are enabled; excluded
variants are not counted as deployment coverage.
## Verification
- All selected test registration names and assertion bodies match the
previous enabled revision, checked by AST comparison.
- Verified that the canary diff contains only the inventoried exclusions
and their obsolete skip plumbing; other exclusions are preserved.
- Formatting and lint passed; 77 gate infrastructure unit tests passed.
- Full local bootstrap was blocked by missing package-level dependencies
in the temporary worktree. Fresh deployment execution on these rewritten
commits remains to be verified in CI.
<details>
<summary>Preserved scope inventory (24)</summary>
- ID 1:
`test/e2e/app-dir/app-client-cache/client-cache.original.test.ts` —
`describe('app dir client cache semantics (30s/5min)', () => {
const { next, isNextDev } = nextTestSetup({
files: path.join(__dirname, 'fixtures', 'regular'),
nextConfig: {
experimental: { staleTimes: { dynamic: 30, static: 180 } },
},
})
if (isNextDev) {
// dev doesn't support prefetch={true}, so this just performs a basic
test to make sure data is reused for 30s
it('should renew the 30s cache once the data is revalidated', async ()
=> {
let browser = await next.browser('/', browserConfigWithFixedTime)
// navigate to prefetch-auto page
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
let initialNumber = await browser.elementById('random-number').text()
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
let newNumber = await browser.elementById('random-number').text()
// the number should be the same, as we navigated within 30s.
expect(newNumber).toBe(initialNumber)
// Fast forward to expire the cache
await browser.eval(fastForwardTo, 30 * 1000)
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
newNumber = await browser.elementById('random-number').text()
// ~35s have passed, so the cache should be expired and the number
should be different
expect(newNumber).not.toBe(initialNumber)
// once the number is updated, we should have a renewed 30s cache for
this entry
// store this new number so we can check that it stays the same
initialNumber = newNumber
await browser.eval(fastForwardTo, 5 * 1000)
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
newNumber = await browser.elementById('random-number').text()
// the number should be the same, as we navigated within 30s (part 2).
expect(newNumber).toBe(initialNumber)
})
} else {
describe('prefetch={true}', () => {
let browser: Playwright
beforeEach(async () => {
browser = await next.browser('/', browserConfigWithFixedTime)
})
it('should prefetch the full page', async () => {
const { getRequests, clearRequests } =
await createRequestsListener(browser)
await retry(() => {
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/0' && !didPartialPrefetch
)
).toBe(true)
})
clearRequests()
await browser.elementByCss('[href="/0?timeout=0"]').click()
await browser.waitForElementByCss('#random-number')
await retry(() => {
const requests = getRequests()
expect(requests.every(([url]) => getPathname(url) !== '/0')).toBe(
true
)
})
})
it('should re-use the cache for the full page, only for 5 mins', async
() => {
await browser.elementByCss('[href="/0?timeout=0"]').click()
await browser.waitForElementByCss('#random-number')
const randomNumber = await browser.elementById('random-number').text()
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/0?timeout=0"]')
await browser.elementByCss('[href="/0?timeout=0"]').click()
await browser.waitForElementByCss('#random-number')
const number = await browser.elementById('random-number').text()
expect(number).toBe(randomNumber)
await browser.eval(fastForwardTo, 5 * 60 * 1000)
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/0?timeout=0"]')
await browser.elementByCss('[href="/0?timeout=0"]').click()
await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()
expect(newNumber).not.toBe(randomNumber)
})
it('should prefetch again after 5 mins if the link is visible again',
async () => {
const { getRequests, clearRequests } =
await createRequestsListener(browser)
await retry(() => {
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/0' && !didPartialPrefetch
)
).toBe(true)
})
await browser.elementByCss('[href="/0?timeout=0"]').click()
await browser.waitForElementByCss('#random-number')
const randomNumber = await browser.elementById('random-number').text()
await browser.eval(fastForwardTo, 5 * 60 * 1000)
clearRequests()
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/0?timeout=0"]')
await retry(() => {
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/0' && !didPartialPrefetch
)
).toBe(true)
})
await browser.elementByCss('[href="/0?timeout=0"]').click()
await browser.waitForElementByCss('#random-number')
const number = await browser.elementById('random-number').text()
expect(number).not.toBe(randomNumber)
})
})
describe('prefetch={false}', () => {
let browser: Playwright
beforeEach(async () => {
browser = await next.browser('/', browserConfigWithFixedTime)
})
it('should not prefetch the page at all', async () => {
const { getRequests } = await createRequestsListener(browser)
await browser.elementByCss('[href="/2"]').click()
await browser.waitForElementByCss('#random-number')
await retry(() => {
const requests = getRequests().filter(
([url]) => getPathname(url) === '/2'
)
expect(requests.length).toBe(1)
})
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/2' && didPartialPrefetch
)
).toBe(false)
})
it('should re-use the cache only for 30 seconds', async () => {
await browser.elementByCss('[href="/2"]').click()
await browser.waitForElementByCss('#random-number')
const randomNumber = await browser.elementById('random-number').text()
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/2"]')
await browser.elementByCss('[href="/2"]').click()
await browser.waitForElementByCss('#random-number')
const number = await browser.elementById('random-number').text()
expect(number).toBe(randomNumber)
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/2"]')
await browser.elementByCss('[href="/2"]').click()
await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()
expect(newNumber).not.toBe(randomNumber)
})
})
describe('prefetch={undefined} - default', () => {
let browser: Playwright
beforeEach(async () => {
browser = await next.browser('/', browserConfigWithFixedTime)
})
it('should prefetch partially a dynamic page', async () => {
const { getRequests, clearRequests } =
await createRequestsListener(browser)
await retry(() => {
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/1' && didPartialPrefetch
)
).toBe(true)
})
clearRequests()
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
await retry(() => {
expect(
getRequests().some(
([url, didPartialPrefetch]) =>
getPathname(url) === '/1' && !didPartialPrefetch
)
).toBe(true)
})
})
it('should re-use the full cache for only 30 seconds', async () => {
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
const randomNumber = await browser.elementById('random-number').text()
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
const number = await browser.elementById('random-number').text()
expect(number).toBe(randomNumber)
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()
expect(newNumber).toBe(randomNumber)
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
const newNumber2 = await browser.elementById('random-number').text()
expect(newNumber2).not.toBe(newNumber)
})
it('should renew the 30s cache once the data is revalidated', async ()
=> {
// navigate to prefetch-auto page
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
let initialNumber = await browser.elementById('random-number').text()
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
let newNumber = await browser.elementById('random-number').text()
// the number should be the same, as we navigated within 30s.
expect(newNumber).toBe(initialNumber)
// Fast forward to expire the cache
await browser.eval(fastForwardTo, 30 * 1000)
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
newNumber = await browser.elementById('random-number').text()
// ~35s have passed, so the cache should be expired and the number
should be different
expect(newNumber).not.toBe(initialNumber)
// once the number is updated, we should have a renewed 30s cache for
this entry
// store this new number so we can check that it stays the same
initialNumber = newNumber
await browser.eval(fastForwardTo, 5 * 1000)
// Navigate back to the index, and then back to the prefetch-auto page
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
newNumber = await browser.elementById('random-number').text()
// the number should be the same, as we navigated within 30s (part 2).
expect(newNumber).toBe(initialNumber)
})
it('should refetch below the fold after 30 seconds', async () => {
await browser.elementByCss('[href="/1?timeout=1000"]').click()
await browser.waitForElementByCss('#random-number')
const randomNumber = await browser.elementById('random-number').text()
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1?timeout=1000"]')
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/1?timeout=1000"]').click()
await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()
expect(newNumber).not.toBe(randomNumber)
})
it('should refetch the full page after 5 mins', async () => {
// Wait for initial prefetch to complete before clicking
await browser.waitForIdleNetwork()
const randomLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
const randomNumber = await browser
.waitForElementByCss('#random-number')
.text()
await browser.eval(fastForwardTo, 5 * 60 * 1000)
await browser
.elementByCss('[href="/"]')
.click()
.waitForElementByCss('[href="/1?timeout=1000"]')
// Wait for prefetch requests to complete before clicking, otherwise
// clicking during an in-flight prefetch aborts it and skips loading
state
await browser.waitForIdleNetwork()
const newLoadingNumber = await browser
.elementByCss('[href="/1?timeout=1000"]')
.click()
.waitForElementByCss('#loading')
.text()
const newNumber = await browser
.waitForElementByCss('#random-number')
.text()
expect(newLoadingNumber).not.toBe(randomLoadingNumber)
expect(newNumber).not.toBe(randomNumber)
})
it('should respect a loading boundary that returns `null`', async () =>
{
await browser.elementByCss('[href="/null-loading"]').click()
// the page content should disappear immediately
await retry(async () => {
expect(
await browser.hasElementByCssSelector('[href="/null-loading"]')
).toBe(false)
})
// the root layout should still be visible
expect(await browser.hasElementByCssSelector('#root-layout')).toBe(true)
// the dynamic content should eventually appear
await browser.waitForElementByCss('#random-number')
expect(await browser.hasElementByCssSelector('#random-number')).toBe(
true
)
})
})
it('should seed the prefetch cache with the fetched page data', async ()
=> {
const browser = await next.browser('/1', browserConfigWithFixedTime)
await browser.waitForElementByCss('#random-number')
const initialNumber = await browser.elementById('random-number').text()
// Move forward a few seconds, navigate off the page and then back to it
await browser.eval(fastForwardTo, 5 * 1000)
await browser.elementByCss('[href="/"]').click()
await browser.waitForElementByCss('[href="/1"]')
await browser.waitForIdleNetwork()
await browser.elementByCss('[href="/1"]').click()
await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()
// The number should be the same as we've seeded it in the prefetch
cache when we loaded the full page
expect(newNumber).toBe(initialNumber)
})
it('should renew the initial seeded data after expiration time', async
() => {
const browser = await next.browser(
'/without-loading/1',
browserConfigWithFixedTime
)
await browser.waitForElementByCss('#random-number')
const initialNumber = await browser.elementById('random-number').text()
// Expire the cache
await browser.eval(fastForwardTo, 30 * 1000)
await browser.elementByCss('[href="/without-loading"]').click()
await browser.waitForElementByCss('[href="/without-loading/1"]')
await browser.elementByCss('[href="/without-loading/1"]').click()
await browser.waitForElementByCss('#random-number')
const newNumber = await browser.elementById('random-number').text()
// The number should be different, as the seeded data has expired after
30s
expect(newNumber).not.toBe(initialNumber)
})
}
})`
- ID 4: `test/e2e/app-dir/app-custom-cache-handler/index.test.ts` —
`describe('app-dir - custom-cache-handler - cjs', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
env: {
CUSTOM_CACHE_HANDLER: 'cache-handler.js',
},
})
runTests('cjs module exports', { next, isNextDev })
})`
- ID 5: `test/e2e/app-dir/app-custom-cache-handler/index.test.ts` —
`describe('app-dir - custom-cache-handler - cjs-default-export', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
env: {
CUSTOM_CACHE_HANDLER: 'cache-handler-cjs-default-export.js',
},
})
runTests('cjs default export', { next, isNextDev })
})`
- ID 6: `test/e2e/app-dir/app-custom-cache-handler/index.test.ts` —
`describe('app-dir - custom-cache-handler - esm', () => {
const { next, isNextDev } = nextTestSetup({
files: {
app: new FileRef(__dirname + '/app'),
'cache-handler-esm.js': new FileRef(__dirname +
'/cache-handler-esm.js'),
'next.config.js': originalNextConfig.replace(
'module.exports = ',
'export default '
),
},
packageJson: {
type: 'module',
},
env: {
CUSTOM_CACHE_HANDLER: 'cache-handler-esm.js',
},
})
runTests('esm default export', { next, isNextDev })
})`
- ID 7: `test/e2e/app-dir/app-custom-cache-handler/index.test.ts` —
`describe('app-dir - custom-cache-handler - esm import.meta.resolve', ()
=> {
const { next, isNextDev } = nextTestSetup({
files: {
app: new FileRef(__dirname + '/app'),
'cache-handler-esm.js': new FileRef(__dirname +
'/cache-handler-esm.js'),
'next.config.js': importMetaResolveNextConfig,
},
packageJson: {
type: 'module',
},
})
runTests('esm default export', { next, isNextDev })
})`
- ID 9: `test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts`
— `describe('app dir - prefetching (custom staleTime)', () => {
const { next, isNextDev } = nextTestSetup({
files: {
app: new FileRef(join(__dirname, 'app')),
},
nextConfig: {
experimental: {
staleTimes: {
static: 30, // Minimum enforced by clientSegmentCache is 30 seconds
dynamic: 5,
},
},
},
})
if (isNextDev) {
it('should skip next dev for now', () => {})
return
}
it('should not fetch again when a static page was prefetched when
navigating to it twice', async () => {
let act: ReturnType<typeof createRouterAct>
const browser = await next.browser('/', {
beforePageLoad(page) {
act = createRouterAct(page)
},
})
// Reveal the link to trigger prefetch and wait for it to complete
const link = await act(
async () => {
const reveal = await browser.elementByCss('#accordion-to-static-page')
await reveal.click()
return browser.elementByCss('#to-static-page')
},
{ includes: 'Static Page [prefetch-sentinel]' }
)
// Navigate to static page - should use prefetched data with no
additional requests
await act(async () => {
await link.click()
const staticPageText = await browser.elementByCss('#static-page').text()
expect(staticPageText).toBe('Static Page [prefetch-sentinel]')
}, 'no-requests')
// Reveal the "to-home" link and navigate back
// Note: Not using act() here because behavior differs between cache
models.
// With clientSegmentCache, revealing may trigger a prefetch. Without
it, home is already
// cached so no prefetch occurs. Either way, navigation works with
cached data.
const reveal = await browser.elementByCss('#accordion-to-home')
await reveal.click()
const homeLink = await browser.waitForElementByCss('#to-home')
await homeLink.click()
await browser.waitForElementByCss('#accordion-to-static-page')
// Reveal the static page link again since accordion is hidden after
navigation
await browser.elementByCss('#accordion-to-static-page').click()
await browser.waitForElementByCss('#to-static-page')
// Navigate to static page again using the accordion - should still use
cached data with no additional requests
const staticPageText = await act(async () => {
await browser.elementByCss('#to-static-page').click()
return browser.elementByCss('#static-page').text()
}, 'no-requests')
expect(staticPageText).toBe('Static Page [prefetch-sentinel]')
})
it('should fetch again when a static page was prefetched when navigating
to it after the stale time has passed', async () => {
let act: ReturnType<typeof createRouterAct>
const timeController = createTimeController()
const browser = await next.browser('/', {
beforePageLoad(page) {
act = createRouterAct(page)
},
})
// Install time controller
await timeController.install(browser)
// Reveal the static-page link to trigger prefetch and wait for it to
complete
let link = await act(
async () => {
const reveal = await browser.elementByCss('#accordion-to-static-page')
await reveal.click()
return browser.elementByCss('#to-static-page')
},
{ includes: 'Static Page [prefetch-sentinel]' }
)
// Navigate to static page - should use prefetched data with no
additional requests
await act(async () => {
await link.click()
await browser.waitForElementByCss('#static-page')
}, 'no-requests')
// Reveal the "to-home" link and navigate back
const reveal = await browser.elementByCss('#accordion-to-home')
await reveal.click()
const homeLink = await browser.waitForElementByCss('#to-home')
await homeLink.click()
await browser.waitForElementByCss('#accordion-to-static-page')
// Advance time past the stale time
await timeController.advance(browser, 31000)
// Reveal the static-page link to trigger prefetch and wait for it to
complete
link = await act(
async () => {
const reveal = await browser.elementByCss('#accordion-to-static-page')
await reveal.click()
return browser.elementByCss('#to-static-page')
},
{ includes: 'Static Page [prefetch-sentinel]' }
)
// Navigate to static page - should use prefetched data with no
additional requests
await act(async () => {
await link.click()
await browser.waitForElementByCss('#static-page')
}, 'no-requests')
})
// FIXME: Flaky test - investigate and re-enable
it.skip('should not re-fetch cached data when navigating back to a route
group', async () => {
let act: ReturnType<typeof createRouterAct>
// Just installing so that the page doesn't automatically move past
dynamic stale time
createTimeController()
const browser = await next.browser('/prefetch-auto-route-groups', {
beforePageLoad(page) {
act = createRouterAct(page)
},
})
// Once the page has loaded, we expect a data fetch (initial page load)
expect(await browser.elementById('count').text()).toBe('1')
// Navigate to a sub-page - this will trigger a data fetch
await act(async () => {
await browser
.elementByCss("[href='/prefetch-auto-route-groups/sub/foo']")
.click()
})
// Navigate back to the route group page - should use cached data with
no additional fetch
await act(async () => {
await
browser.elementByCss("[href='/prefetch-auto-route-groups']").click()
// Confirm that the dashboard page is still rendering the stale fetch
count, as it should be cached
}, 'no-requests')
expect(await browser.elementById('count').text()).toBe('1')
// Navigate to a new sub-page - this will trigger another data fetch
await act(async () => {
await browser
.elementByCss("[href='/prefetch-auto-route-groups/sub/bar']")
.click()
})
// Finally, go back to the route group page - should use cached data
with no additional fetch
await act(async () => {
await
browser.elementByCss("[href='/prefetch-auto-route-groups']").click()
}, 'no-requests')
// Confirm that the dashboard page is still rendering the stale fetch
count, as it should be cached
expect(await browser.elementById('count').text()).toBe('1')
// Reload the page to get the accurate total number of fetches
await browser.refresh()
// The initial fetch, 2 sub-page fetches, and a final fetch when
reloading the page
expect(await browser.elementById('count').text()).toBe('4')
})
it('should fetch again when the initially visited static page is visited
after the stale time has passed', async () => {
let act: ReturnType<typeof createRouterAct>
const timeController = createTimeController()
const browser = await next.browser('/static-page-no-prefetch', {
beforePageLoad(page) {
act = createRouterAct(page)
},
})
// Install time controller
await timeController.install(browser)
// Wait for the page to load (initial navigation request happened during
browser load)
await browser.waitForElementByCss('#static-page-no-prefetch')
// Reveal the home link and wait for prefetch to complete, then navigate
const homeLink = await act(
async () => {
const reveal = await browser.elementByCss('#accordion-to-home')
await reveal.click()
return browser.elementByCss('#to-home')
},
{ includes: 'Home Page [prefetch-sentinel]' }
)
// Navigate to home - no additional requests since we just prefetched
await homeLink.click()
await browser.waitForElementByCss('#accordion-to-static-page')
// Advance time past the stale time
await timeController.advance(browser, 31000)
// Reveal the link to static-page-no-prefetch and wait for prefetch
const link = await act(
async () => {
const reveal = await browser.elementByCss(
'#accordion-to-static-page-no-prefetch'
)
await reveal.click()
return browser.elementByCss('#to-static-page-no-prefetch')
},
{ includes: 'Static Page No Prefetch [prefetch-sentinel]' }
)
// Navigate back to static-page-no-prefetch - should use the fresh
prefetch data
const staticPageText = await act(async () => {
await link.click()
return browser.elementByCss('#static-page-no-prefetch').text()
}, 'no-requests')
expect(staticPageText).toBe('Static Page No Prefetch
[prefetch-sentinel]')
})
it('should renew the stale time after refetching expired RSC data',
async () => {
let act: ReturnType<typeof createRouterAct>
const timeController = createTimeController()
const browser = await next.browser('/', {
beforePageLoad(page) {
act = createRouterAct(page)
},
})
// Install time controller
await timeController.install(browser)
// Reveal the static-page link to trigger prefetch and wait for it to
complete
let link = await act(
async () => {
const reveal = await browser.elementByCss('#accordion-to-static-page')
await reveal.click()
return browser.elementByCss('#to-static-page')
},
{ includes: 'Static Page [prefetch-sentinel]' }
)
// Navigate to static page (should use prefetched data with no
additional requests)
await act(async () => {
await link.click()
await browser.waitForElementByCss('#static-page')
}, 'no-requests')
// Reveal the "to-home" link and navigate back
// Note: Not using act() here because behavior differs between cache
models.
// With clientSegmentCache, revealing may trigger a prefetch. Without
it, home is already
// cached so no prefetch occurs. Either way, navigation works with
cached data.
const reveal = await browser.elementByCss('#accordion-to-home')
await reveal.click()
const homeLink = await browser.waitForElementByCss('#to-home')
await homeLink.click()
await browser.waitForElementByCss('#accordion-to-static-page')
// Advance time past the stale time
await timeController.advance(browser, 31000)
// Reveal the static-page link to trigger prefetch and wait for it to
complete
link = await act(
async () => {
const reveal = await browser.elementByCss('#accordion-to-static-page')
await reveal.click()
return browser.elementByCss('#to-static-page')
},
{ includes: 'Static Page [prefetch-sentinel]' }
)
// Navigate to static page again (should use freshly prefetched data
with no additional requests)
await act(async () => {
await link.click()
await browser.waitForElementByCss('#static-page')
}, 'no-requests')
// Go back to home (reveal the link and navigate)
// Note: Not using act() here because behavior differs between cache
models.
const reveal2 = await browser.elementByCss('#accordion-to-home')
await reveal2.click()
const homeLink2 = await browser.waitForElementByCss('#to-home')
await homeLink2.click()
await browser.waitForElementByCss('#accordion-to-static-page')
// Advance time but not past the stale time (20 seconds < 30 second
stale time - should still be fresh)
await timeController.advance(browser, 20000)
// Reveal the static-page link to trigger prefetch (should use cached
data, not refetch)
link = await act(async () => {
const reveal = await browser.elementByCss('#accordion-to-static-page')
await reveal.click()
return browser.elementByCss('#to-static-page')
}, 'no-requests')
// Navigate to static page again (should NOT refetch - stale time should
be renewed)
// If this assertion passes, it means the stale time was properly
renewed after the refetch
const staticPageText = await act(async () => {
await link.click()
return browser.elementByCss('#static-page').text()
}, 'no-requests')
expect(staticPageText).toBe('Static Page [prefetch-sentinel]')
})
})`
- ID 10: `test/e2e/app-dir/app-root-params-getters/use-cache.test.ts` —
`describe('app-root-param-getters - cache dedup with root params', () =>
{
const { next, isNextDev } = nextTestSetup({
files: join(__dirname, 'fixtures', 'use-cache-dedup'),
})
it('should dedupe same root params and isolate different root params',
async () => {
// Three concurrent requests: ca/en, ca/fr, ca/fr.
const [$en, $fr1, $fr2] = await Promise.all([
next.render$('/ca/en'),
next.render$('/ca/fr'),
next.render$('/ca/fr'),
])
const randomEn = $en('#random').text()
const randomFr1 = $fr1('#random').text()
const randomFr2 = $fr2('#random').text()
expect(randomEn).toBeTruthy()
expect(randomFr1).toBeTruthy()
// ca/en and ca/fr should have different results (isolation).
expect(randomEn).not.toBe(randomFr1)
// Both ca/fr requests should have the same result (deduped).
expect(randomFr1).toBe(randomFr2)
})
it('should dedupe same root params and isolate different root params for
private caches', async () => {
// Three concurrent requests: ca/en, ca/fr, ca/fr.
const [$en, $fr1, $fr2] = await Promise.all([
next.render$('/ca/en/use-cache-private'),
next.render$('/ca/fr/use-cache-private'),
next.render$('/ca/fr/use-cache-private'),
])
const randomEn = $en('#random').text()
const randomFr1 = $fr1('#random').text()
const randomFr2 = $fr2('#random').text()
expect(randomEn).toBeTruthy()
expect(randomFr1).toBeTruthy()
// Different root params produce different entries, in dev and
production.
expect(randomEn).not.toBe(randomFr1)
if (isNextDev) {
// In dev, private caches are persisted and participate in cross-request
// deduplication keyed by root params, so the two ca/fr requests join
one
// in-flight invocation and share a single fill.
expect(randomFr1).toBe(randomFr2)
} else {
// In production, private caches are not persisted and are never deduped
// across requests, so each ca/fr request generates its own value.
expect(randomFr1).not.toBe(randomFr2)
}
})
})`
- ID 21: `test/e2e/app-dir/cache-components-errors/module-scope.test.ts`
— `describe('Lazy Module Init', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname + '/fixtures/lazy-module-init',
skipStart: true,
})
if (isNextDev) {
it('does not run in dev', () => {})
return
}
it('should build statically even if module scope uses sync APIs like
current time and random', async () => {
try {
await next.start()
} catch {
throw new Error('expected build not to fail for fully static project')
}
expect(next.cliOutput).toContain('○ /server')
expect(next.cliOutput).toContain('○ /client')
expect(next.cliOutput).toContain('○ /client-page')
expect(next.cliOutput).toContain('◐ /[dyn]')
let $
$ = await next.render$('/server')
expect($('#id').text().length).toBeGreaterThan(0)
$ = await next.render$('/client')
expect($('#id').text().length).toBeGreaterThan(0)
$ = await next.render$('/client-page')
expect($('#id').text().length).toBeGreaterThan(0)
$ = await next.render$('/foo')
expect($('#id').text().length).toBeGreaterThan(0)
$ = await next.render$('/serial-client-sync-io')
expect($('#id').text().length).toBeGreaterThan(0)
})
})`
- ID 27:
`test/e2e/app-dir/cache-components/cache-components.connection.test.ts`
— `describe('cache-components', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
it('should partially prerender pages that use connection', async () => {
let $ = await next.render$('/connection/static-behavior/boundary', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#foo').text()).toBe('foo')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#foo').text()).toBe('foo')
}
})
it('should be able to pass connection as a promise to another component
and trigger an intermediate Suspense boundary', async () => {
const $ = await next.render$('/connection/static-behavior/pass-deeply')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
// In dev, whether or not the fallback appears in the HTML is unreliable
// and depends on timing, so we don't assert on its presence
// (if we want to assert on it, we should use a browser test)
expect($('#page').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#fallback').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at runtime')
}
})
})`
- ID 28:
`test/e2e/app-dir/cache-components/cache-components.cookies.test.ts` —
`describe('cache-components', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
it('should partially prerender pages that use cookies', async () => {
let $ = await next.render$('/cookies/static-behavior', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#x-sentinel').text()).toBe('hello')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#x-sentinel').text()).toBe('hello')
}
})
it('should be able to pass cookies as a promise to another component and
trigger an intermediate Suspense boundary', async () => {
const $ = await next.render$('/cookies/static-behavior/pass-deeply')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#fallback').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#fallback').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at runtime')
}
})
it('should be able to access cookie properties', async () => {
let $ = await next.render$('/cookies/exercise', {})
let cookieWarnings = next.cliOutput
.split('\n')
.filter((l) => l.includes('Route "/cookies/exercise'))
expect(cookieWarnings).toHaveLength(0)
// For...of iteration
expect($('#for-of-x-sentinel').text()).toContain('hello')
expect($('#for-of-x-sentinel-path').text()).toContain('/cookies/exercise')
expect($('#for-of-x-sentinel-rand').text()).toContain('x-sentinel-rand')
// ...spread iteration
expect($('#spread-x-sentinel').text()).toContain('hello')
expect($('#spread-x-sentinel-path').text()).toContain('/cookies/exercise')
expect($('#spread-x-sentinel-rand').text()).toContain('x-sentinel-rand')
// cookies().size
expect(parseInt($('#size-cookies').text())).toBeGreaterThanOrEqual(3)
// cookies().get('...') && cookies().getAll('...')
expect($('#get-x-sentinel').text()).toContain('hello')
expect($('#get-x-sentinel-path').text()).toContain('/cookies/exercise')
expect($('#get-x-sentinel-rand').text()).toContain('x-sentinel-rand')
// cookies().has('...')
expect($('#has-x-sentinel').text()).toContain('true')
expect($('#has-x-sentinel-foobar').text()).toContain('false')
// cookies().set('...', '...')
expect($('#set-result-x-sentinel').text()).toContain(
'Cookies can only be modified in a Server Action'
)
expect($('#set-value-x-sentinel').text()).toContain('hello')
// cookies().delete('...', '...')
expect($('#delete-result-x-sentinel').text()).toContain(
'Cookies can only be modified in a Server Action'
)
expect($('#delete-value-x-sentinel').text()).toContain('hello')
// cookies().clear()
expect($('#clear-result').text()).toContain(
'Cookies can only be modified in a Server Action'
)
expect($('#clear-value-x-sentinel').text()).toContain('hello')
// cookies().toString()
expect($('#toString').text()).toContain('x-sentinel=hello')
expect($('#toString').text()).toContain('x-sentinel-path')
expect($('#toString').text()).toContain('x-sentinel-rand=')
})
})`
- ID 29:
`test/e2e/app-dir/cache-components/cache-components.date.test.ts` —
`describe('cache-components', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
it('should not have route specific errors', async () => {
expect(next.cliOutput).not.toMatch('Error: Route "/')
expect(next.cliOutput).not.toMatch('Error occurred prerendering page')
})
it('should prerender pages with cached `Date.now()` calls', async () =>
{
let $ = await next.render$('/date/now/cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#value').text()).toMatch(/^\d+$/)
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#value').text()).toMatch(/^\d+$/)
}
})
it('should prerender pages with cached `Date()` calls', async () => {
let $ = await next.render$('/date/date/cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#value').text()).toContain('GMT')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#value').text()).toContain('GMT')
}
})
it('should prerender pages with cached `new Date()` calls', async () =>
{
let $ = await next.render$('/date/new-date/cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#value').text()).toContain('GMT')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#value').text()).toContain('GMT')
}
})
it('should prerender pages with cached static Date instances like `new
Date(0)`', async () => {
let $ = await next.render$('/date/static-date/cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#value').text()).toContain('GMT')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#value').text()).toContain('GMT')
}
})
it('should not prerender pages with uncached static Date instances like
`new Date(0)`', async () => {
let $ = await next.render$('/date/static-date/uncached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#value').text()).toContain('GMT')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#value').text()).toContain('GMT')
}
})
})`
- ID 30:
`test/e2e/app-dir/cache-components/cache-components.draft-mode.test.ts`
— `describe('cache-components', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
let cliIndex = 0
beforeEach(() => {
cliIndex = next.cliOutput.length
})
function getLines(containing: string): Array<string> {
const warnings = next.cliOutput
.slice(cliIndex)
.split('\n')
.filter((l) => l.includes(containing))
cliIndex = next.cliOutput.length
return warnings
}
it('should fully prerender pages that use draftMode', async () => {
expect(getLines('Route "/draftmode')).toEqual([])
let $ = await next.render$('/draftmode', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#draft-mode').text()).toBe('false')
expect(getLines('Route "/draftmode')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#draft-mode').text()).toBe('false')
expect(getLines('Route "/draftmode')).toEqual([])
}
})
if (!isNextDev) {
it('should stream Suspense fallbacks when draft mode is enabled', async
() => {
const draftRes = await next.fetch('/draftmode/toggle')
const setCookie = draftRes.headers.get('set-cookie')
const cookieHeader = { Cookie: setCookie?.split(';', 1)[0] }
expect(cookieHeader.Cookie).toBeTruthy()
const $ = await next.render$('/draftmode/streaming', undefined, {
headers: cookieHeader,
})
expect($('#draft-mode').text()).toBe('true')
expect($('#delayed-runtime-fallback').text()).toBe(
'Loading draft content...'
)
})
}
})`
- ID 32:
`test/e2e/app-dir/cache-components/cache-components.node-crypto.test.ts`
— `describe('cache-components', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
it('should not have route specific errors', async () => {
expect(next.cliOutput).not.toMatch('Error: Route "/')
expect(next.cliOutput).not.toMatch('Error occurred prerendering page')
})
it("should prerender pages with cached
`require('node:crypto').getRandomValues(...)` calls", async () => {
let $ = await next.render$('/node-crypto/get-random-values/cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#first').text()).not.toEqual($('#second').text())
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#first').text()).not.toEqual($('#second').text())
}
})
it("should prerender pages with cached
`require('node:crypto').randomUUID()` calls", async () => {
let $ = await next.render$('/node-crypto/random-uuid/cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#first').text()).not.toEqual($('#second').text())
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#first').text()).not.toEqual($('#second').text())
}
})
it("should prerender pages with cached
`require('node:crypto').randomBytes(size)` calls", async () => {
let $ = await next.render$('/node-crypto/random-bytes/cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#first').text()).not.toEqual($('#second').text())
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#first').text()).not.toEqual($('#second').text())
}
})
it("should prerender pages with cached
`require('node:crypto').randomFillSync(buffer)` calls", async () => {
let $ = await next.render$('/node-crypto/random-fill-sync/cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#first').text()).not.toEqual($('#second').text())
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#first').text()).not.toEqual($('#second').text())
}
})
it("should prerender pages with cached
`require('node:crypto').randomInt(max)` calls", async () => {
let $ = await next.render$('/node-crypto/random-int/up-to/cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#first').text()).not.toEqual($('#second').text())
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#first').text()).not.toEqual($('#second').text())
}
})
it("should prerender pages with cached
`require('node:crypto').randomInt(min, max)` calls", async () => {
let $ = await next.render$('/node-crypto/random-int/between/cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#first').text()).not.toEqual($('#second').text())
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#first').text()).not.toEqual($('#second').text())
}
})
it("should prerender pages with cached
`require('node:crypto').generatePrimeSync(size, options)` calls", async
() => {
let $ = await next.render$('/node-crypto/generate-prime-sync/cached',
{})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#first').text()).not.toEqual($('#second').text())
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#first').text()).not.toEqual($('#second').text())
}
})
it("should prerender pages with cached
`require('node:crypto').generateKeyPairSync(type, options)` calls",
async () => {
let $ = await next.render$('/node-crypto/generate-key-pair-sync/cached',
{})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#first').text()).not.toEqual($('#second').text())
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#first').text()).not.toEqual($('#second').text())
}
})
it("should prerender pages with cached
`require('node:crypto').generateKeySync(type, options)` calls", async ()
=> {
let $ = await next.render$('/node-crypto/generate-key-sync/cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#first').text()).not.toEqual($('#second').text())
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#first').text()).not.toEqual($('#second').text())
}
})
})`
- ID 33:
`test/e2e/app-dir/cache-components/cache-components.params.test.ts` —
`describe('cache-components', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
let cliIndex = 0
beforeEach(() => {
cliIndex = next.cliOutput.length
})
function getLines(containing: string): Array<string> {
const warnings = next.cliOutput
.slice(cliIndex)
.split('\n')
.filter((l) => l.includes(containing))
cliIndex = next.cliOutput.length
return warnings
}
describe('Params', () => {
it('should partially prerender pages that await params in a server
components', async () => {
expect(getLines('Route "/params')).toEqual([])
let $ = await next.render$(
'/params/semantics/one/build/layout-access/server'
)
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('build')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('build')
}
$ = await next.render$('/params/semantics/one/run/layout-access/server')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('run')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
expect($('#page').text()).toBe('at runtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('run')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/build/page-access/server')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('build')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('build')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/run/page-access/server')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('run')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
expect($('#page').text()).toBe('at runtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('run')
expect(getLines('Route "/params')).toEqual([])
}
})
// Since #85155, we intentionally omit search params from client
segments
// if the page is otherwise static, and resume using a client fetch
// instead. So it's expected that the value is missing pre-hydration.
// There are separate tests that verify that it is eventually hydrated.
// TODO: Rewrite or update this test.
it.skip('should partially prerender pages that use params in a client
components', async () => {
expect(getLines('Route "/params')).toEqual([])
let $ = await next.render$(
'/params/semantics/one/build/layout-access/client'
)
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('build')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('build')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/run/layout-access/client')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('run')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
expect($('#page').text()).toBe('at runtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('run')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/build/page-access/client')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('build')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('build')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/run/page-access/client')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('run')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
expect($('#page').text()).toBe('at runtime')
expect($('#param-lowcard').text()).toBe('one')
expect($('#param-highcard').text()).toBe('run')
expect(getLines('Route "/params')).toEqual([])
}
})
it('should fully prerender pages that check individual param keys after
awaiting params in a server component', async () => {
expect(getLines('Route "/params')).toEqual([])
let $ = await next.render$(
'/params/semantics/one/build/layout-has/server'
)
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/build/page-has/server')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/run/layout-has/server')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
} else {
// With PPR fallbacks the first visit is still partially prerendered
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
expect($('#page').text()).toBe('at runtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/run/page-has/server')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
} else {
// With PPR fallbacks the first visit is still partially prerendered
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
expect($('#page').text()).toBe('at runtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
}
})
// Since #85155, we intentionally omit search params from client
segments
// if the page is otherwise static, and resume using a client fetch
// instead. So it's expected that the value is missing pre-hydration.
// There are separate tests that verify that it is eventually hydrated.
// TODO: Rewrite or update this test.
it.skip('should fully prerender pages that check individual param keys
after `use`ing params in a client component', async () => {
expect(getLines('Route "/params')).toEqual([])
let $ = await next.render$(
'/params/semantics/one/build/layout-has/client'
)
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/build/page-has/client')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/run/layout-has/client')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
} else {
// With PPR fallbacks the first visit is still partially prerendered
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
expect($('#page').text()).toBe('at runtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/run/page-has/client')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
} else {
// With PPR fallbacks the first visit is still partially prerendered
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
expect($('#page').text()).toBe('at runtime')
expect($('#param-has-lowcard').text()).toBe('true')
expect($('#param-has-highcard').text()).toBe('true')
expect($('#param-has-foo').text()).toBe('false')
expect(getLines('Route "/params')).toEqual([])
}
})
it('should partially prerender pages that spread awaited params in a
server component', async () => {
expect(getLines('Route "/params')).toEqual([])
let $ = await next.render$(
'/params/semantics/one/build/layout-spread/server'
)
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('build')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('build')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/build/page-spread/server')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('build')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('build')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/run/layout-spread/server')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('run')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
expect($('#page').text()).toBe('at runtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('run')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/run/page-spread/server')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('run')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
expect($('#page').text()).toBe('at runtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('run')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
}
})
// Since #85155, we intentionally omit search params from client
segments
// if the page is otherwise static, and resume using a client fetch
// instead. So it's expected that the value is missing pre-hydration.
// There are separate tests that verify that it is eventually hydrated.
// TODO: Rewrite or update this test.
it.skip('should partially prerender pages that spread `use`ed params in
a client component', async () => {
expect(getLines('Route "/params')).toEqual([])
let $ = await next.render$(
'/params/semantics/one/build/layout-spread/client'
)
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('build')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('build')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/build/page-spread/client')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('build')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('build')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/run/layout-spread/client')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('run')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
expect($('#page').text()).toBe('at runtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('run')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/semantics/one/run/page-spread/client')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#lowcard').text()).toBe('at runtime')
expect($('#highcard').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('run')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#lowcard').text()).toBe('at buildtime')
expect($('#highcard').text()).toBe('at buildtime')
expect($('#highcard-fallback').text()).toBe('loading highcard children')
expect($('#page').text()).toBe('at runtime')
expect($('#param-copied-lowcard').text()).toBe('one')
expect($('#param-copied-highcard').text()).toBe('run')
expect($('#param-key-count').text()).toBe('2')
expect(getLines('Route "/params')).toEqual([])
}
})
})
describe('Param Shadowing', () => {
it('should correctly allow param names like then, value, and status when
awaiting params in a server component', async () => {
expect(getLines('Route "/params')).toEqual([])
let $ = await next.render$(
'/params/shadowing/foo/bar/baz/qux/layout/server'
)
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-dyn').text()).toBe('foo')
expect($('#param-then').text()).toBe('bar')
expect($('#param-value').text()).toBe('baz')
expect($('#param-status').text()).toBe('qux')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-dyn').text()).toBe('foo')
expect($('#param-then').text()).toBe('bar')
expect($('#param-value').text()).toBe('baz')
expect($('#param-status').text()).toBe('qux')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/shadowing/foo/bar/baz/qux/page/server')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-dyn').text()).toBe('foo')
expect($('#param-then').text()).toBe('bar')
expect($('#param-value').text()).toBe('baz')
expect($('#param-status').text()).toBe('qux')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-dyn').text()).toBe('foo')
expect($('#param-then').text()).toBe('bar')
expect($('#param-value').text()).toBe('baz')
expect($('#param-status').text()).toBe('qux')
expect(getLines('Route "/params')).toEqual([])
}
})
// Since #85155, we intentionally omit search params from client
segments
// if the page is otherwise static, and resume using a client fetch
// instead. So it's expected that the value is missing pre-hydration.
// There are separate tests that verify that it is eventually hydrated.
// TODO: Rewrite or update this test.
it.skip('should correctly allow param names like then, value, and status
when `use`ing params in a client component', async () => {
expect(getLines('Route "/params')).toEqual([])
let $ = await next.render$(
'/params/shadowing/foo/bar/baz/qux/layout/client'
)
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-dyn').text()).toBe('foo')
expect($('#param-then').text()).toBe('bar')
expect($('#param-value').text()).toBe('baz')
expect($('#param-status').text()).toBe('qux')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-dyn').text()).toBe('foo')
expect($('#param-then').text()).toBe('bar')
expect($('#param-value').text()).toBe('baz')
expect($('#param-status').text()).toBe('qux')
expect(getLines('Route "/params')).toEqual([])
}
$ = await next.render$('/params/shadowing/foo/bar/baz/qux/page/client')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-dyn').text()).toBe('foo')
expect($('#param-then').text()).toBe('bar')
expect($('#param-value').text()).toBe('baz')
expect($('#param-status').text()).toBe('qux')
expect(getLines('Route "/params')).toEqual([])
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at runtime')
expect($('#param-dyn').text()).toBe('foo')
expect($('#param-then').text()).toBe('bar')
expect($('#param-value').text()).toBe('baz')
expect($('#param-status').text()).toBe('qux')
expect(getLines('Route "/params')).toEqual([])
}
})
})
if (!isNextDev) {
describe('generateStaticParams', () => {
// This test is skipped as the previous workaround of using
`fetch-cache` will no longer be supported with DIO.
it.skip('should have cacheComponents semantics inside
generateStaticParams', async () => {
// This test is named what we want but our current implementation is not
actually correct yet.
// We are asserting current behavior and will update the test when we
land the correct behavior
const lines: Array<string> = next.cliOutput.split('\n')
let i = 0
while (true) {
const line = lines[i++]
if (typeof line !== 'string') {
throw new Error(
'Could not find expected route output for
/params/generate-static-params/[slug]/page/...'
)
}
if (
line.startsWith('├') &&
line.includes('/params/generate-static-params/[slug]')
) {
let nextLine = lines[i++]
// we expect the fallback shell first
expect(nextLine).toContain('/params/generate-static-params/[slug]')
nextLine = lines[i++]
expect(nextLine).toMatch(
/\/params\/generate-static-params\/\d+\/page/
)
nextLine = lines[i++]
// Because we force-cache we only end up with one prebuilt page.
// When cacheComponents semantics are fully respected we will end up
with two.
expect(nextLine).not.toMatch(
/\/params\/generate-static-params\/\d+\/page/
)
break
}
}
})
})
}
})`
- ID 34:
`test/e2e/app-dir/cache-components/cache-components.random.test.ts` —
`describe('cache-components', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
it('should not have route specific errors', async () => {
expect(next.cliOutput).not.toMatch('Error: Route "/')
expect(next.cliOutput).not.toMatch('Error occurred prerendering page')
})
it('should prerender pages with cached Math.random() calls', async () =>
{
let $ = await next.render$('/random/cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
}
})
})`
- ID 35:
`test/e2e/app-dir/cache-components/cache-components.routes.test.ts` —
`describe('cache-components', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
let cliIndex = 0
beforeEach(() => {
cliIndex = next.cliOutput.length
})
function getLines(containing: string): Array<string> {
const warnings = next.cliOutput
.slice(cliIndex)
.split('\n')
.filter((l) => l.includes(containing))
cliIndex = next.cliOutput.length
return warnings
}
it('should not prerender GET route handlers that use dynamic APIs',
async () => {
let str = await next.render('/routes/dynamic-cookies', {})
let json = JSON.parse(str)
expect(json.value).toEqual('at runtime')
expect(json.type).toEqual('cookies')
str = await next.render('/routes/dynamic-headers', {})
json = JSON.parse(str)
expect(json.value).toEqual('at runtime')
expect(json.type).toEqual('headers')
str = await next.render('/routes/dynamic-stream', {})
json = JSON.parse(str)
expect(json.value).toEqual('at runtime')
expect(json.message).toEqual('dynamic stream')
str = await next.render('/routes/dynamic-url?foo=bar', {})
json = JSON.parse(str)
expect(json.value).toEqual('at runtime')
expect(json.search).toEqual('?foo=bar')
})
it('should prerender GET route handlers that have entirely cached io
(fetches)', async () => {
let str = await next.render('/routes/fetch-cached', {})
let json = JSON.parse(str)
let random1 = json.random1
let random2 = json.random2
if (isNextDev) {
expect(json.value).toEqual('at runtime')
expect(typeof random1).toBe('string')
expect(typeof random2).toBe('string')
} else {
expect(json.value).toEqual('at buildtime')
expect(typeof random1).toBe('string')
expect(typeof random2).toBe('string')
}
str = await next.render('/routes/fetch-cached', {})
json = JSON.parse(str)
if (isNextDev) {
expect(json.value).toEqual('at runtime')
expect(random1).toEqual(json.random1)
expect(random2).toEqual(json.random2)
} else {
expect(json.value).toEqual('at buildtime')
expect(random1).toEqual(json.random1)
expect(random2).toEqual(json.random2)
}
})
it('should not prerender GET route handlers that have some uncached io
(fetches)', async () => {
let str = await next.render('/routes/fetch-mixed', {})
let json = JSON.parse(str)
let random1 = json.random1
let random2 = json.random2
expect(json.value).toEqual('at runtime')
expect(typeof random1).toBe('string')
expect(typeof random2).toBe('string')
str = await next.render('/routes/fetch-mixed', {})
json = JSON.parse(str)
expect(json.value).toEqual('at runtime')
expect(random1).toEqual(json.random1)
expect(random2).not.toEqual(json.random2)
})
it('should prerender GET route handlers that have entirely cached io
(unstable_cache)', async () => {
let str = await next.render('/routes/io-cached', {})
let json = JSON.parse(str)
let message1 = json.message1
let message2 = json.message2
if (isNextDev) {
expect(json.value).toEqual('at runtime')
expect(typeof message1).toBe('string')
expect(typeof message2).toBe('string')
} else {
expect(json.value).toEqual('at buildtime')
expect(typeof message1).toBe('string')
expect(typeof message2).toBe('string')
}
str = await next.render('/routes/io-cached', {})
json = JSON.parse(str)
if (isNextDev) {
expect(json.value).toEqual('at runtime')
expect(message1).toEqual(json.message1)
expect(message2).toEqual(json.message2)
} else {
expect(json.value).toEqual('at buildtime')
expect(message1).toEqual(json.message1)
expect(message2).toEqual(json.message2)
}
})
it('should prerender GET route handlers that have entirely cached io
("use cache")', async () => {
let str = await next.render('/routes/use_cache-cached', {})
let json = JSON.parse(str)
let message1 = json.message1
let message2 = json.message2
if (isNextDev) {
expect(json.value).toEqual('at runtime')
expect(typeof message1).toBe('string')
expect(typeof message2).toBe('string')
} else {
expect(json.value).toEqual('at buildtime')
expect(typeof message1).toBe('string')
expect(typeof message2).toBe('string')
}
str = await next.render('/routes/use_cache-cached', {})
json = JSON.parse(str)
if (isNextDev) {
expect(json.value).toEqual('at runtime')
expect(message1).toEqual(json.message1)
expect(message2).toEqual(json.message2)
} else {
expect(json.value).toEqual('at buildtime')
expect(message1).toEqual(json.message1)
expect(message2).toEqual(json.message2)
}
})
it('should not prerender GET route handlers that have some uncached io
(unstable_cache)', async () => {
let str = await next.render('/routes/io-mixed', {})
let json = JSON.parse(str)
let message1 = json.message1
let message2 = json.message2
expect(json.value).toEqual('at runtime')
expect(typeof message1).toBe('string')
expect(typeof message2).toBe('string')
str = await next.render('/routes/io-mixed', {})
json = JSON.parse(str)
expect(json.value).toEqual('at runtime')
expect(message1).toEqual(json.message1)
expect(message2).not.toEqual(json.message2)
})
it('should prerender GET route handlers that complete synchronously or
in a microtask', async () => {
let str = await next.render('/routes/microtask', {})
let json = JSON.parse(str)
if (isNextDev) {
expect(json.value).toEqual('at runtime')
expect(json.message).toBe('microtask')
} else {
expect(json.value).toEqual('at buildtime')
expect(json.message).toBe('microtask')
}
str = await next.render('/routes/static-stream-sync', {})
json = JSON.parse(str)
if (isNextDev) {
expect(json.value).toEqual('at runtime')
expect(json.message).toBe('stream response')
} else {
expect(json.value).toEqual('at buildtime')
expect(json.message).toBe('stream response')
}
str = await next.render('/routes/static-stream-async', {})
json = JSON.parse(str)
if (isNextDev) {
expect(json.value).toEqual('at runtime')
expect(json.message).toBe('stream response')
} else {
expect(json.value).toEqual('at buildtime')
expect(json.message).toBe('stream response')
}
str = await next.render('/routes/static-string-sync', {})
json = JSON.parse(str)
if (isNextDev) {
expect(json.value).toEqual('at runtime')
expect(json.message).toBe('string response')
} else {
expect(json.value).toEqual('at buildtime')
expect(json.message).toBe('string response')
}
str = await next.render('/routes/static-string-async', {})
json = JSON.parse(str)
if (isNextDev) {
expect(json.value).toEqual('at runtime')
expect(json.message).toBe('string response')
} else {
expect(json.value).toEqual('at buildtime')
expect(json.message).toBe('string response')
}
})
it('should not prerender GET route handlers that complete in a new
Task', async () => {
let str = await next.render('/routes/task', {})
let json = JSON.parse(str)
expect(json.value).toEqual('at runtime')
expect(json.message).toBe('task')
})
it('should prerender GET route handlers when accessing params', async ()
=> {
expect(getLines('Route "/routes/[dyn]')).toEqual([])
let str = await next.render('/routes/1', {})
let json = JSON.parse(str)
if (isNextDev) {
expect(json.value).toEqual('at runtime')
expect(json.type).toBe('dynamic params')
expect(json.param).toBe('1')
expect(getLines('Route "/routes/[dyn]')).toEqual([])
} else {
expect(json.value).toEqual('at buildtime')
expect(json.type).toBe('dynamic params')
expect(json.param).toBe('1')
expect(getLines('Route "/routes/[dyn]')).toEqual([])
}
str = await next.render('/routes/2', {})
json = JSON.parse(str)
if (isNextDev) {
expect(json.value).toEqual('at runtime')
expect(json.type).toBe('dynamic params')
expect(json.param).toBe('2')
expect(getLines('Route "/routes/[dyn]')).toEqual([])
} else {
expect(json.value).toEqual('at runtime')
expect(json.type).toBe('dynamic params')
expect(json.param).toBe('2')
expect(getLines('Route "/routes/[dyn]')).toEqual([])
}
})
})`
- ID 36:
`test/e2e/app-dir/cache-components/cache-components.search.test.ts` —
`describe('cache-components', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
it('should partially prerender pages that await searchParams in a server
component', async () => {
let $ = await next.render$('/search/server/await?sentinel=hello')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#value').text()).toBe('hello')
expect($('#page').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('main').text()).toContain('inner loading...')
expect($('main').text()).not.toContain('outer loading...')
expect($('#value').text()).toBe('hello')
expect($('#page').text()).toBe('at runtime')
}
})
it('should partially prerender pages that `use` searchParams in a server
component', async () => {
let $ = await next.render$('/search/server/use?sentinel=hello')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#value').text()).toBe('hello')
expect($('#page').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('main').text()).toContain('inner loading...')
expect($('main').text()).not.toContain('outer loading...')
expect($('#value').text()).toBe('hello')
expect($('#page').text()).toBe('at runtime')
}
})
it('should partially prerender pages that `use` searchParams in a client
component', async () => {
let $ = await next.render$('/search/client/use?sentinel=hello')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#value').text()).toBe('hello')
expect($('#page').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('main').text()).toContain('inner loading...')
expect($('main').text()).not.toContain('outer loading...')
// Since #85155, we intentionally omit search params from client
segments
// if the page is otherwise static, and resume using a client fetch
// instead. So it's expected that the value is missing pre-hydration.
// There are separate tests that verify that it is eventually hydrated.
// TODO: Rewrite or update this test.
// expect($('#value').text()).toBe('hello')
// expect($('#page').text()).toBe('at runtime')
expect($('#value').text()).toBe('')
expect($('#page').text()).toBe('')
}
})
})`
- ID 37: `test/e2e/app-dir/cache-components/cache-components.test.ts` —
`describe('cache-components', () => {
const { next, isNextDev, isNextStart } = nextTestSetup({
files: __dirname,
})
it('should not have route specific errors', async () => {
expect(next.cliOutput).not.toMatch('Error: Route "/')
expect(next.cliOutput).not.toMatch('Error occurred prerendering page')
})
if (isNextDev) {
it('should not log not-found errors', async () => {
const cliOutputLength = next.cliOutput.length
await next.browser('/cases/not-found')
const cliOutput = next.cliOutput.slice(cliOutputLength)
expect(cliOutput).not.toMatch('Error: NEXT_HTTP_ERROR_FALLBACK;404')
expect(cliOutput).not.toMatch('unhandledRejection')
})
} else {
it('should not warn about potential memory leak for even listeners on
AbortSignal', async () => {
expect(next.cliOutput).not.toMatch('MaxListenersExceededWarning')
})
}
it('should prerender fully static pages', async () => {
let $ = await next.render$('/cases/static', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
}
$ = await next.render$('/cases/static_async', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
}
})
it('should prerender static not-found pages', async () => {
// Using `browser` instead of `render$` because error pages must be
hydrated
// apparently.
const browser = await next.browser('/cases/not-found')
if (isNextDev) {
expect(await browser.elementById('layout').text()).toBe('at runtime')
expect(await browser.elementById('page').text()).toBe('at runtime')
} else {
expect(await browser.elementById('layout').text()).toBe('at buildtime')
expect(await browser.elementById('page').text()).toBe('at buildtime')
}
})
it('should render not-found with Suspense in layout without connection
errors', async () => {
const browser = await next.browser('/cases/not-found-suspense')
// The custom not-found component should render
expect(await browser.elementById('not-found-text').text()).toBe(
'Custom 404 - Not Found'
)
// The async Suspense content in the layout should also render
expect(await browser.elementById('async-data').text()).toBe(
'Async Data Loaded'
)
})
it('should prerender pages that render in a microtask', async () => {
let $ = await next.render$('/cases/microtask', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
}
$ = await next.render$('/cases/microtask_deep_tree', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
}
})
it('should partially prerender pages that take longer than a task to
render', async () => {
let $ = await next.render$('/cases/task', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#inner').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
// The inner slot is computed during the prerender but is hidden
// it gets revealed when the resume happens
expect($('#inner').text()).toBe('at buildtime')
}
})
it('should prerender pages that only use cached fetches', async () => {
const $ = await next.render$('/cases/fetch_cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
}
})
it('should partially prerender pages that use at least one fetch without
cache', async () => {
let $ = await next.render$('/cases/fetch_mixed', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#inner').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#inner').text()).toBe('at buildtime')
}
})
it('should prerender pages that only use cached (unstable_cache) IO',
async () => {
const $ = await next.render$('/cases/io_cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
}
})
it('should prerender pages that only use cached ("use cache") IO', async
() => {
const $ = await next.render$('/cases/use_cache_cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
}
})
it('should prerender pages that cached the whole page', async () => {
const $ = await next.render$('/cases/full_cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
}
})
it('should partially prerender pages that do any uncached IO', async ()
=> {
let $ = await next.render$('/cases/io_mixed', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#inner').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#inner').text()).toBe('at buildtime')
}
})
it('should partially prerender pages that do any uncached IO (use
cache)', async () => {
let $ = await next.render$('/cases/use_cache_mixed', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#inner').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#inner').text()).toBe('at buildtime')
}
})
it('should partially prerender pages that use `cookies()`', async () =>
{
let $ = await next.render$('/cases/dynamic_api_cookies', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#inner').text()).toBe('at runtime')
expect($('#value').text()).toBe('hello')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#inner').text()).toBe('at buildtime')
expect($('#value').text()).toBe('hello')
}
})
it('should partially prerender pages that use `headers()`', async () =>
{
let $ = await next.render$('/cases/dynamic_api_headers')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#inner').text()).toBe('at runtime')
expect($('#value').text()).toBe('hello')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#inner').text()).toBe('at buildtime')
expect($('#value').text()).toBe('hello')
}
})
it('should fully prerender pages that use `unstable_noStore()`', async
() => {
let $ = await next.render$('/cases/dynamic_api_no_store', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#inner').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#inner').text()).toBe('at buildtime')
}
})
it('should partially prerender pages that use `searchParams` in Server
Components', async () => {
let $ = await next.render$(
'/cases/dynamic_api_search_params_server?sentinel=my+sentinel',
{}
)
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#inner').text()).toBe('at runtime')
expect($('#value').text()).toBe('my sentinel')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#inner').text()).toBe('at buildtime')
expect($('#value').text()).toBe('my sentinel')
}
})
it('should partially prerender pages that use `searchParams` in Client
Components', async () => {
let $ = await next.render$(
'/cases/dynamic_api_search_params_client?sentinel=my+sentinel',
{}
)
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#inner').text()).toBe('at runtime')
expect($('#value').text()).toBe('my sentinel')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
// The second component renders before the first one aborts so we end up
// capturing the static value during buildtime
expect($('#inner').text()).toBe('at buildtime')
// Since there was no dynamic data access on this page, the search
params
// are completely ommitted from the HTML document and filled in by
// the client
expect($('#value').text()).toBe('')
expect($('#fallback-component-one-').text()).toBe('loading...')
}
})
it('can prerender pages with parallel routes that are static', async ()
=> {
const $ = await next.render$('/cases/parallel/static', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page-slot').text()).toBe('at runtime')
expect($('#page-children').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page-slot').text()).toBe('at buildtime')
expect($('#page-children').text()).toBe('at buildtime')
}
})
it('can prerender pages with parallel routes that resolve in a
microtask', async () => {
const $ = await next.render$('/cases/parallel/microtask', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page-slot').text()).toBe('at runtime')
expect($('#page-children').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page-slot').text()).toBe('at buildtime')
expect($('#page-children').text()).toBe('at buildtime')
}
})
it('does not prerender pages with parallel routes that resolve in a
task', async () => {
const $ = await next.render$('/cases/parallel/task', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page-slot').text()).toBe('at runtime')
expect($('#page-children').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page-slot').text()).toBe('at runtime')
expect($('#page-children').text()).toBe('at buildtime')
}
})
it('does not prerender pages with parallel routes that uses a dynamic
API', async () => {
let $ = await next.render$('/cases/parallel/no-store', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page-slot').text()).toBe('at runtime')
expect($('#page-children').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page-slot').text()).toBe('at buildtime')
expect($('#page-children').text()).toBe('at buildtime')
}
$ = await next.render$('/cases/parallel/cookies', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page-slot').text()).toBe('at runtime')
expect($('#page-children').text()).toBe('at runtime')
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page-slot').text()).toBe('at runtime')
expect($('#page-children').text()).toBe('at buildtime')
}
})
if (isNextStart) {
it('should ignore late setHeader calls for direct RSC handlers after
headers are sent', async () => {
const pageModulePath = path.join(
next.testDir,
'.next',
'server',
'app',
'cases',
'static',
'page.js'
)
const previousCwd = process.cwd()
const port = await findPort()
let server: http.Server | undefined
let handlerError: unknown
let lateHeaderAttempted = false
let lateHeaderError: unknown
let resolveHandled: (() => void) | undefined
const handled = new Promise<void>((resolve) => {
resolveHandled = resolve
})
try {
process.chdir(next.testDir)
const { handler } = require(pageModulePath) as {
handler: (
req: http.IncomingMessage,
res: http.ServerResponse,
ctx: {
requestMeta?: Record<string, unknown>
waitUntil?: (promise: Promise<void>) => void
}
) => Promise<void>
}
server = http.createServer(async (req, res) => {
const originalWriteHead = res.writeHead.bind(res)
res.writeHead = ((...args: any[]) => {
const result = originalWriteHead(...args)
if (!lateHeaderAttempted) {
lateHeaderAttempted = true
try {
res.setHeader('x-test-late', '1')
} catch (error) {
lateHeaderError = error
}
}
return result
}) as typeof res.writeHead
try {
await handler(req, res, {
waitUntil: () => {},
requestMeta: {
initURL: `https://localhost:${port}${req.url ?? '/'}`,
minimalMode: true,
relativeProjectDir: '.',
},
})
} catch (error) {
handlerError = error
if (!res.writableEnded) {
if (!res.headersSent) {
res.statusCode = 500
}
res.end()
}
} finally {
resolveHandled?.()
}
})
await new Promise<void>((resolve, reject) => {
server.listen(port, () => {
resolve()
})
server.once('error', reject)
})
const stateTree = JSON.stringify(['', {}])
const requestUrl = new URL('/cases/static', `http://localhost:${port}`)
const cacheBustingParam = await computeCacheBustingSearchParam(
undefined,
undefined,
stateTree,
undefined
)
if (cacheBustingParam) {
requestUrl.searchParams.set('_rsc', cacheBustingParam)
}
const res = await fetchViaHTTP(
port,
requestUrl.pathname + requestUrl.search,
undefined,
{
headers: {
rsc: '1',
'next-router-state-tree': stateTree,
},
redirect: 'manual',
}
)
const flight = await res.text()
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('text/x-component')
await handled
expect(handlerError).toBeUndefined()
expect(lateHeaderAttempted).toBe(true)
expect(lateHeaderError).toBeUndefined()
expect(flight.length).toBeGreaterThan(0)
} finally {
process.chdir(previousCwd)
if (server) {
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error)
return
}
resolve()
})
})
}
}
})
}
it('should not resume when client components are dynamic but the RSC
render was static', async () => {
let html = await next.render('/cases/static-rsc-dynamic-client', {})
const $ = cheerio.load(html)
// Confirm the HTML document was sent completely
expect(html).toContain('</body></html>')
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
// In dev we SSR the time
expect($('#time').length).toBe(1)
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
// Confirm the time span is not part of the completed HTML document
expect($('#time').length).toBe(0)
}
const browser = await next.browser('/cases/static-rsc-dynamic-client')
const now = new Date()
if (isNextDev) {
expect(await browser.elementById('layout').text()).toBe('at runtime')
expect(await browser.elementById('page').text()).toBe('at runtime')
// Assert that we rendered a time within the last couple seconds.
const inPageDate = new Date(
await browser.waitForElementByCss('#time').text()
)
expect(inPageDate.getTime() - now.getTime()).toBeLessThan(2000)
} else {
expect(await browser.elementById('layout').text()).toBe('at buildtime')
expect(await browser.elementById('page').text()).toBe('at buildtime')
// Assert that we rendered a time within the last 2 seconds.
const inPageDate = new Date(
await browser.waitForElementByCss('#time').text()
)
expect(inPageDate.getTime() - now.getTime()).toBeLessThan(2000)
}
})
})`
- ID 38:
`test/e2e/app-dir/cache-components/cache-components.web-crypto.test.ts`
— `describe('cache-components', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
it('should not have route specific errors', async () => {
expect(next.cliOutput).not.toMatch('Error: Route "/')
expect(next.cliOutput).not.toMatch('Error occurred prerendering page')
})
it('should prerender pages with cached `crypto.getRandomValues(...)`
calls', async () => {
let $ = await next.render$('/web-crypto/get-random-values/cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#first').text()).not.toEqual($('#second').text())
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#first').text()).not.toEqual($('#second').text())
}
})
it('should prerender pages with cached `crypto.randomUUID()` calls',
async () => {
let $ = await next.render$('/web-crypto/random-uuid/cached', {})
if (isNextDev) {
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
expect($('#first').text()).not.toEqual($('#second').text())
} else {
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#first').text()).not.toEqual($('#second').text())
}
})
})`
- ID 40:
`test/e2e/app-dir/instant-validation-static-shells/instant-validation-static-shells.test.ts`
— `describe('instant validation - opting out of static shells', () => {
const { next, isNextDev } = nextTestSetup({
files: join(__dirname, 'fixtures', 'valid'),
})
// NOTE: if something's wrong in build, we'll fail before any tests run.
// Visiting the pages is mostly just a sanity check.
it('does not require a static shell if a root layouts is configured as
blocking', async () => {
const browser = await next.browser('/blocking-root-layout')
await browser.elementByCss('main')
if (isNextDev) await waitForNoErrorToast(browser)
})
it('does not require a static shell if a layout is configured as
blocking', async () => {
const browser = await next.browser('/blocking-layout')
await browser.elementByCss('main')
if (isNextDev) await waitForNoErrorToast(browser)
})
it('does not require a static shell if a page is configured as
blocking', async () => {
const browser = await next.browser('/blocking-page')
await browser.elementByCss('main')
if (isNextDev) await waitForNoErrorToast(browser)
})
})`
- ID 42:
`test/e2e/app-dir/non-rsc-router-prefetch/non-rsc-router-prefetch.test.ts`
— `describe('non-rsc-router-prefetch', () => {
const { next } = nextTestSetup({
files: __dirname,
})
beforeAll(async () => {
const res = await next.fetch('/')
await res.text()
})
it('ignores the router prefetch header for HTML requests', async () => {
const res = await next.fetch('/', {
headers: {
[NEXT_ROUTER_PREFETCH_HEADER]: '1',
},
signal: AbortSignal.timeout(5_000),
})
const html = await res.text()
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('text/html')
expect(html).toContain('hello world')
})
it('honors the router prefetch header for RSC requests', async () => {
const res = await next.fetch('/', {
headers: {
[RSC_HEADER]: '1',
[NEXT_ROUTER_PREFETCH_HEADER]: '1',
},
})
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toContain('text/x-component')
})
})`
- ID 56:
`test/e2e/app-dir/use-cache-infinity-profile/use-cache-infinity-profile.test.ts`
— `describe('use-cache-infinity-profile', () => {
const { next, isNextStart } = nextTestSetup({
files: __dirname,
})
it('caches forever with a configured profile using Infinity revalidate
and expire', async () => {
const $ = await next.render$('/')
const initialValue = $('#value').text()
expect(initialValue).toMatch(uuidRegExp)
// An infinite cache life must not degrade into a dynamic cache life, so
// the value stays the same across requests instead of regenerating.
const $second = await next.render$('/')
expect($second('#value').text()).toBe(initialValue)
if (isNextStart) {
// The page must be fully prerendered at build time.
const prerendered = await next.readFile('.next/server/app/index.html')
expect(prerendered).toContain(initialValue)
}
})
it('serves an inline Infinity cache life from a JSON-backed cache
handler across requests', async () => {
const $ = await next.render$('/inline?key=a')
const initialValue = $('#value').text()
expect(initialValue).toMatch(uuidRegExp)
// The second request reads the entry back from the cache handler. If
the
// infinite cache life doesn't survive the handler's JSON round trip,
the
// entry is treated as immediately expired and the value regenerates.
const $second = await next.render$('/inline?key=a')
expect($second('#value').text()).toBe(initialValue)
})
})`
- ID 57:
`test/e2e/app-dir/use-cache-og-image-top-level-await/use-cache-og-image-top-level-await.test.ts`
— `describe('use-cache-og-image-top-level-await', () => {
const { next, isNextStart } = nextTestSetup({
files: __dirname,
skipStart: true,
})
if (isNextStart) {
beforeAll(async () => {
await next.build({ args: ['--experimental-build-mode', 'compile'] })
})
it('should prerender a page whose opengraph image uses a top-level
await', async () => {
const { exitCode, cliOutput } = await next.build({
args: [
'--experimental-build-mode',
'generate',
'--debug-build-paths',
'app/[slug]/page.tsx,app/[slug]/opengraph-image.tsx',
],
})
expect(cliOutput).not.toContain(
'Unexpected cache miss after cache warming phase'
)
expect(cliOutput).not.toContain(
'Next.js encountered uncached or runtime data in `generateMetadata()`'
)
expect(exitCode).toBe(0)
// The image route uses generateStaticParams, so the build is expected
// to prerender it for each param.
expect(cliOutput).toMatch(/● \/first-post\/opengraph-image/)
expect(cliOutput).toMatch(/● \/second-post\/opengraph-image/)
})
} else {
beforeAll(async () => {
await next.start()
})
it('should render a page whose opengraph image uses a top-level await',
async () => {
const $ = await next.render$('/first-post')
expect($('article').text()).toBe('First Post')
const res = await next.fetch('/first-post/opengraph-image')
expect(res.status).toBe(200)
expect(res.headers.get('content-type')).toBe('image/png')
})
}
})`
- ID 58:
`test/e2e/app-dir/use-cache-output-export/use-cache-output-export.test.ts`
— `describe('use-cache-output-export', () => {
const { next, isNextStart } = nextTestSetup({
files: __dirname,
skipStart: process.env.NEXT_TEST_MODE !== 'dev',
})
if (process.env.__NEXT_CACHE_COMPONENTS === 'true') {
return it.skip('for PPR', () => {
// PPR is not compatible with `output: 'export'`.
})
}
it('should work', async () => {
let html: string
let server: Server | undefined
if (isNextStart) {
const { cliOutput } = await next.build()
expect(cliOutput).not.toInclude(
'Server Actions are not supported with static export.'
)
server = await startCleanStaticServer(join(next.testDir, 'out'))
const { port } = server.address() as AddressInfo
html = await renderViaHTTP(port, '/')
} else {
html = await next.render('/')
}
expect(html).toMatch(/<p>[0,1]\.\d+<\/p>/)
if (server) {
await new Promise((resolve) => server.close(resolve))
}
})
})`
</details>
<details>
<summary>Deployment evidence for the additional scopes</summary>
- `test/e2e/app-dir/app-prefetch/prefetching.stale-times.test.ts` —
`describe('app dir - prefetching (custom staleTime)', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710370);
Cache Components excluded by manifest.
- `test/e2e/app-dir/cache-components-errors/module-scope.test.ts` —
`describe('Lazy Module Init', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710410);
Cache Components excluded by manifest.
- `test/e2e/app-dir/cache-components/cache-components.params.test.ts` —
`describe('cache-components', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710449);
Cache Components excluded by manifest.
-
`test/e2e/app-dir/instant-validation-static-shells/instant-validation-static-shells.test.ts`
— `describe('instant validation - opting out of static shells', () =>
{`:
[normal](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710456),
[cache](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710457).
-
`test/e2e/app-dir/use-cache-output-export/use-cache-output-export.test.ts`
— `describe('use-cache-output-export', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710496),
[cache](https://github.com/vercel/next.js/actions/runs/34426785098/job/102728710503);
Cache Components explicitly skipped for PPR.
</details>
<!-- NEXT_JS_LLM -->
|
||
|
|
1a295a631c |
Align fallback parameter staging with shell validation (#98512)
`next dev` reported missing Suspense boundaries in layouts that the
build accepted. For `/[top]/items/[bottom]`, `generateStaticParams`
returned `[{ top: 't1' }]`, and the page wrapped its `bottom` access in
Suspense. A request for `/t2/items/b2` still reported the layout's
access to `top` as an error. Development treated both parameters as
unresolved because the requested `top` value was not generated, although
the required static shell only needed to defer `bottom`.
Production Cached Navigations used the same overly broad parameter set
and omitted eligible static content from repeat visits. Resumes also
reconstructed that set from the original build manifest, even after an
on-demand prerender had produced a more complete shell.
This replaces the alternative proposed in #98460. That proposal fixes
the development error by selecting a separate fallback parameter set for
validation while keeping response staging unchanged. The two sets are
not intended to differ for the same shell target. Correcting only
validation would preserve the incorrect staging decision and leave
production Cached Navigations without the eligible static content.
Staging and static-shell validation now use one `stagedFallbackParams`
set for each selected shell target. Required partial shells retain their
unresolved parameters, even when a later request can complete them.
Prerenders record their parameter set in postponed state, and resumes
use that recorded set rather than reconstructing it from the generic
source.
Dynamic RSC requests now read and revalidate the completed-shell cache
key, so they find partial artifacts that the fully resolved pathname
lookup missed. Request metadata and `RequestStore` both expose the set
as `stagedFallbackParams`. Action-only fallback detection checks actual
unresolved parameters instead of treating deferred values as missing.
|
||
|
|
1f60b5f193 |
test: restore deployment exclusion for shared Cache Components tests (#98464)
## Summary Restore `skipDeployment` and the early return in the shared Cache Components error-test runner. The migration in #98154 replaced them with a force-gate pragma, but the gate transformer only processes `.test.*` files, so the pragma in `shared.util.ts` did not preserve the deployment exclusion. This is a temporary rollback to unblock deploy testing. Moving the test definitions out of the helper and migrating them again will be evaluated separately; this PR does not change the transformer or refactor the tests. ## Verification - Ran all 12 importing test files through Jest in deploy mode with deployment setup mocked to throw before any fixture or deployment work. Before the rollback, all 12 reached that guard and failed. After the rollback, all 12 take the legacy exclusion path and no deployment setup is attempted. - `pnpm typescript --skipLibCheck` passes. - Full type-check reports the existing nine Octokit dependency declaration errors; no errors in the changed file. <!-- NEXT_JS_LLM --> |
||
|
|
e0966df114 |
test: migrate caching deploy exclusions to force gates (#98154)
## Summary - migrate Cache Components, PPR, prefetching, prerendering, revalidation, and SSG deployment exclusions to `@force-gate` - remove the corresponding `skipDeployment` options and `skipped` control flow while preserving each suite's rationale This keeps caching-related exclusions together so their deployed semantics can be reviewed by the same owners. ## Verification - verified on the combined top-of-stack tree with `pnpm typescript` - compared ordinary-mode collection before and after: all 4,670 existing test names matched - collected all 510 affected files in deploy mode: 4,578 skipped tests, zero failures <!-- NEXT_JS_LLM --> |
||
|
|
e9180eae2a |
errors: shorten "use cache" messages and unify them into one factory (#94300)
### What? Centralizes related <code>"use cache"</code> scope errors and rewrites them as shorter, actionable messages with consistent <code>Learn more:</code> links. It also updates the reachable revalidation errors for rendering and <code>generateStaticParams</code>. ### Why? The previous messages were long, inconsistent, and sometimes missing documentation links. Some new cache-specific revalidation messages were also hidden by an earlier render-phase error. The new messages name the constraint, give the immediate fix, and link to the relevant documentation. ### How? Adds centralized error factories and dedicated error pages for request data, cache configuration, private-cache composition, and revalidation. When an active App Router route is available, the messages include it. The error pages use consistent terminology and complete examples. The <code>unstable_cache()</code> reference now documents the request-dependent operations that these errors reject. ### Before and after Route-based examples use <code>/products</code>. Bracketed values are alternatives in matching order. For example, <code>[E1482, E1486, E1489]</code> maps to <code>[headers(), cookies(), request.url]</code>. Each runtime error contains one alternative, but the table groups messages with the same template. #### Messages that include the route | Cases | Before | After | | --- | --- | --- | | E1480 · <code>searchParams</code> in <code>"use cache"</code> | Route /products used <code>searchParams</code> inside "use cache". Accessing dynamic request data inside a cache scope is not supported. If you need some search params inside a cached function await <code>searchParams</code> outside of the cached function and pass only the required search params as arguments to the cached function. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache | Route "/products": <code>searchParams</code> can't be read inside <code>"use cache"</code>. Await it outside the cached function and pass what you need as an argument.<br><br>Learn more: https://nextjs.org/docs/messages/next-request-in-use-cache | | [E1482, E1486, E1489] · [<code>headers()</code>, <code>cookies()</code>, <code>request.url</code>] in <code>"use cache"</code> | Route /products used [<code>headers()</code>, <code>cookies()</code>, <code>request.url</code>] inside "use cache". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use [<code>headers()</code>, <code>cookies()</code>, <code>request.url</code>] outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache | Route "/products": [<code>headers()</code>, <code>cookies()</code>, <code>request.url</code>] can't be read inside <code>"use cache"</code>. Read it outside the cached function and pass what you need as an argument.<br><br>Learn more: https://nextjs.org/docs/messages/next-request-in-use-cache | | [E1481, E1485, E1492] · [<code>cookies()</code>, <code>request.url</code>, <code>headers()</code>] in <code>unstable_cache()</code> | Route /products used [<code>cookies()</code>, <code>request.url</code>, <code>headers()</code>] inside a function cached with <code>unstable_cache()</code>. Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use [<code>cookies()</code>, <code>request.url</code>, <code>headers()</code>] outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache | Route "/products": [<code>cookies()</code>, <code>request.url</code>, <code>headers()</code>] can't be read inside <code>unstable_cache()</code>. Read it outside the cached function and pass what you need as an argument.<br><br>Learn more: https://nextjs.org/docs/app/api-reference/functions/unstable_cache | | [E1484, E1491] · <code>draftMode().enable()</code> in [<code>unstable_cache()</code>, <code>"use cache"</code>] | Route /products used "draftMode().enable()" inside [a function cached with <code>unstable_cache()</code>, "use cache"]. The enabled status of <code>draftMode()</code> can be read in caches but you must not enable or disable <code>draftMode()</code> inside a cache. See more info here: [https://nextjs.org/docs/app/api-reference/functions/unstable_cache, https://nextjs.org/docs/messages/next-request-in-use-cache] | Route "/products": <code>draftMode().enable()</code> can't be called inside [<code>unstable_cache()</code>, <code>"use cache"</code>]. Draft mode can be read inside a cached function, but enabling or disabling it must happen outside.<br><br>Learn more: [https://nextjs.org/docs/app/api-reference/functions/unstable_cache, https://nextjs.org/docs/messages/next-request-in-use-cache] | | [E1488, E1499] · <code>connection()</code> in [<code>unstable_cache()</code>, <code>"use cache"</code>] | Route /products used <code>connection()</code> inside [a function cached with <code>unstable_cache()</code>, "use cache"]. The <code>connection()</code> function is used to indicate the subsequent code must only run when there is an actual request, but caches must be able to be produced before a request, so this function is not allowed in this scope. See more info here: [https://nextjs.org/docs/app/api-reference/functions/unstable_cache, https://nextjs.org/docs/messages/next-request-in-use-cache] | Route "/products": <code>connection()</code> can't be called inside [<code>unstable_cache()</code>, <code>"use cache"</code>] because cached functions may run during prerendering, without an incoming request. Call it outside the cached function.<br><br>Learn more: [https://nextjs.org/docs/app/api-reference/functions/unstable_cache, https://nextjs.org/docs/messages/next-request-in-use-cache] | | E1494 · <code>connection()</code> in <code>"use cache: private"</code> | Route /products used <code>connection()</code> inside "use cache: private". The <code>connection()</code> function is used to indicate the subsequent code must only run when there is an actual navigation request, but caches must be able to be produced before a navigation request, so this function is not allowed in this scope. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache | Route "/products": <code>connection()</code> can't be called inside <code>"use cache: private"</code> because private cached functions may run during prefetching, without a navigation request. Call it outside the cached function.<br><br>Learn more: https://nextjs.org/docs/app/api-reference/directives/use-cache-private | | [E1483, E1495] · <code>revalidateTag("products")</code> in [<code>"use cache"</code>, <code>unstable_cache()</code>] | Route /products used "revalidateTag products" inside [a "use cache", a function cached with "unstable_cache(...)"] which is unsupported. To ensure revalidation is performed consistently it must always happen outside of renders and cached functions. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering | Route "/products": <code>revalidateTag("products")</code> can't be called during render, inside a cached function, or inside <code>generateStaticParams</code>. Call it from a Server Action or Route Handler instead.<br><br>Learn more: https://nextjs.org/docs/messages/revalidate-in-use-cache | | During render | Route /products used "revalidateTag products" during render which is unsupported. To ensure revalidation is performed consistently it must always happen outside of renders and cached functions. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering | Route "/products": <code>revalidateTag("products")</code> can't be called during render, inside a cached function, or inside <code>generateStaticParams</code>. Call it from a Server Action or Route Handler instead.<br><br>Learn more: https://nextjs.org/docs/messages/revalidate-in-use-cache | | Inside <code>generateStaticParams</code> | Route /products used "revalidateTag products" inside <code>generateStaticParams</code> which is unsupported. To ensure revalidation is performed consistently it must always happen outside of renders and cached functions. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering | Route "/products": <code>revalidateTag("products")</code> can't be called inside <code>generateStaticParams</code>. Call it from a Server Action or Route Handler instead.<br><br>Learn more: https://nextjs.org/docs/messages/revalidate-in-use-cache | #### Other messages | Cases | Before | After | | --- | --- | --- | | [E1477, E1490] · Nested cache with [short <code>expire</code>, <code>revalidate: 0</code>] | A "use cache" with [short <code>expire</code> (under 5 minutes), zero <code>revalidate</code>] is nested inside another "use cache" that has no explicit <code>cacheLife</code>, which is not allowed during prerendering. Add <code>cacheLife()</code> to the outer "use cache" to choose whether it should be prerendered [with longer <code>expire</code>, with non-zero <code>revalidate</code>] or remain dynamic [with short <code>expire</code>, with zero <code>revalidate</code>]. Read more: https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife | Route <code>"/products"</code>: A nested <code>"use cache"</code> with [a short <code>expire</code> (under 5 minutes), <code>revalidate: 0</code>] is inside an outer <code>"use cache"</code> that has no <code>cacheLife()</code>. Add <code>cacheLife()</code> to the outer one to choose whether to prerender it [with a longer <code>expire</code>, with a non-zero <code>revalidate</code>] or keep it dynamic [with a short <code>expire</code>, with <code>revalidate: 0</code>].<br><br>Learn more: https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife | | E1478 · External promise | Filling a "use cache" entry appears to be stuck on shared state from the outer render scope. The same function completed when run in isolation, which usually means a module-scoped value (for example a top-level Map used to dedupe fetches) is joining a promise created outside the cache. "use cache" already dedupes calls with the same arguments within a request and across requests on the same server instance, so the surrounding dedupe layer is both unnecessary and the likely cause. Remove it and rely on "use cache" alone for deduping.<br><br><em>No documentation link.</em> | Route <code>"/products"</code>: A <code>"use cache"</code> function is awaiting a promise created outside it. The same call completed when run in isolation, so a module-scoped value (often a top-level <code>Map</code> used to dedupe fetches) is most likely blocking it. <code>"use cache"</code> already dedupes calls with the same arguments. Remove the surrounding dedupe layer.<br><br>Learn more: https://nextjs.org/docs/messages/next-request-in-use-cache | | [E1479, E1498] · [<code>cacheTag()</code>, <code>cacheLife()</code>] outside a cached function | [<code>cacheTag()</code>, <code>cacheLife()</code>] can only be called inside a "use cache" function.<br><br><em>No documentation link.</em> | Route <code>"/products"</code>: [<code>cacheTag()</code>, <code>cacheLife()</code>] can only be called inside a <code>"use cache"</code> or <code>"use cache: private"</code> function.<br><br>Learn more: [https://nextjs.org/docs/messages/cache-tag-outside-use-cache, https://nextjs.org/docs/messages/cache-life-outside-use-cache]<br><br>Outside an App Router route, the same message is shown without the route prefix. | | E1487 · Prerender timeout | Filling a cache during prerender timed out, likely because request-specific arguments such as params, searchParams, cookies() or dynamic data were used inside "use cache".<br><br><em>No documentation link.</em> | Route <code>"/products"</code>: A <code>"use cache"</code> function took too long during prerendering. The most common cause is passing unresolved request-specific arguments, such as <code>params</code> or <code>searchParams</code>, into the cached function. Resolve the data before calling the function and pass only the values you need.<br><br>Learn more: https://nextjs.org/docs/messages/next-request-in-use-cache | | E1493 · Private cache inside a public cache | "use cache: private" must not be used within "use cache". It can only be nested inside of another "use cache: private".<br><br><em>No documentation link.</em> | Route <code>"/products"</code>: <code>"use cache: private"</code> can't be nested inside <code>"use cache"</code> because a shared cached function can't depend on private request data. Nest it only inside another <code>"use cache: private"</code>.<br><br>Learn more: https://nextjs.org/docs/messages/use-cache-private-composition | | E1496 · Private cache without a request | "use cache: private" cannot be used outside of a request context.<br><br><em>No documentation link.</em> | Route <code>"/products"</code>: <code>"use cache: private"</code> needs an active request, so it can't be used during <code>generateStaticParams</code> or other build-time contexts. Move it to a request-time component or function.<br><br>Learn more: https://nextjs.org/docs/messages/use-cache-private-composition | | E1497 · Private cache inside <code>unstable_cache()</code> | "use cache: private" must not be used within <code>unstable_cache()</code>.<br><br><em>No documentation link.</em> | Route <code>"/products"</code>: <code>"use cache: private"</code> can't be used inside <code>unstable_cache()</code> because <code>unstable_cache()</code> uses a shared cache that can't contain private request data. Call the private cached function outside <code>unstable_cache()</code>.<br><br>Learn more: https://nextjs.org/docs/messages/use-cache-private-composition | ### Runtime verification - Exercised the 13 route-based message IDs through minimal dev-runtime reproductions. Twelve reached the new factories from userland, with byte-identical output across three requests each. - The `request.url` error for `"use cache"` Route Handlers is currently limited to the prerender path. A dynamic request captured by a cached closure can bypass that tracking; this is existing framework behavior to follow up separately. - Passing the `searchParams` promise into a nested cached function can still surface the earlier synchronous dynamic-API error before this factory. The new message is verified when the cache scope reads its own `searchParams` value. - A follow-up preview verification triggered 17 of the 19 rewritten messages from userland with byte-identical output. The timeout-driven external-promise and prerender-timeout messages were verified in source and through their focused test coverage. ### Verification - <code>CI=1 pnpm build-all</code> - <code>pnpm --filter=next types</code> - <code>pnpm --filter=next build</code> - <code>HEADLESS=true pnpm test-dev-turbo test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts</code> - <code>HEADLESS=true pnpm test-start-turbo test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts</code> - <code>NEXT_SKIP_ISOLATE=1 HEADLESS=true pnpm test-dev-webpack test/e2e/app-dir/cache-components-errors/use-cache.test.ts -t 'cacheLife with (expire < 5 minutes|revalidate: 0)'</code> (6 tests and 6 snapshots passed) - <code>pnpm test-dev test/e2e/app-dir/cache-components-errors/use-cache.test.ts --projects jest.config.*</code> (42 tests and 42 snapshots passed across Turbopack and webpack) - <code>pnpm test-dev test/e2e/app-dir/use-cache-hanging/use-cache-hanging.test.ts --projects jest.config.*</code> (10 tests and 8 snapshots passed across Turbopack and webpack) - <code>pnpm test-dev test/e2e/app-dir/use-cache-configured-timeout/use-cache-configured-timeout.test.ts --projects jest.config.*</code> (4 tests and 2 snapshots passed across Turbopack and webpack) - Prettier, ESLint, and Alex on the changed source, tests, and error pages <!-- NEXT_JS_LLM --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d7aa66c345 |
Remove generated error codes (#97687)
### Why? Should come up with better solution that does not block PRs with git conflict x-ref: https://vercel.slack.com/archives/C02CDC2ALJH/p1785263902728189?thread_ts=1785263687.502649&cid=C02CDC2ALJH ### How? - Delete `errors.json`, the error-code SWC plugin, generated WASM, merge driver, and validation/build tooling. - Stop attaching error codes to server-rendering digests, redboxes, and telemetry; native `Error.code` and `Error.name` remain available where applicable. - Remove the development-overlay error feedback UI, middleware, and telemetry event that depended on stable codes. - Update fixtures, snapshots, and guidance for code-free errors and numeric-only digests. <!-- NEXT_JS_LLM --> |
||
|
|
e3a78fd3c1 |
refactor: move useDynamic{Route,Search}Params to reduce snapshot churn (#97360)
Relocates `useDynamicRouteParams` and `useDynamicSearchParams` to avoid having to update `client-hook-abort-reasons.test.ts` every time there's changes in `dynamic-rendering.ts`. Webpack leaked the source location of the `React.use` calls inside, so any code change that moved the source of the hook would require a snapshot update. For reasons i don't quite understand, moving the hooks to a different file improved the ignore-listing, i.e. we no longer point to the internals of the hooks. This is surprising but not unwelcome. |
||
|
|
268ae984aa |
Remove legacy PPR code paths (#96868)
## Summary `experimental.ppr` is deprecated, and Cache Components is now the only internal path that enables partial prerendering. This makes the legacy PPR renderer and its `prerender-ppr` work unit unreachable. This removes that rendering path, the associated work-unit branches, Next.js’s remaining integration with React’s deprecated `unstable_postpone` API, the dead `__NEXT_PPR` client flag, and obsolete legacy PPR tests. Cache Components’ postponed-state and resume behavior remain unchanged. <!-- NEXT_JS_LLM --> |
||
|
|
d1123c92aa |
Make legacy PPR paths explicit (#96753)
## Summary Make the remaining legacy PPR implementation explicit by renaming the render capability, prerender store, component, and tracking helper around `React.unstable_postpone`. `isLegacyPPR` is only enabled for route-level PPR without Cache Components. The existing `prerender-ppr` work-unit discriminator and rendering behavior are preserved, making this deprecated path easier to audit and eventually remove. |
||
|
|
3cd6d4dd1c |
Track whether runtime data is accessed during prefetch (#95964)
Adds a server-computed signal to static per-segment prefetch responses that tells the client whether a runtime prefetch request would return more content than the static response already contains. The signal is computed by tracking whether the prerender accessed any data source that hangs during a static prerender but would resolve during a runtime prerender. For example: cookies, headers, fallback params, and search params. The information is encoded two places: - As a prefetch hint called ShouldAttemptStaticPrefetch. This tells the client that it's worth attempting to do a static prefetch instead of a runtime one. It's only an optimization, though: if the static response ends up being insufficient, the client will follow it up with a runtime request. It's semantically OK if there are false positives. - Embedded in the static segment response. This tells the client whether the static response is missing data that would have been included in a runtime response. Unlike the prefetch hint, this value must never falsely claim that no runtime request is needed. A follow-up will update the client to skip the runtime request using this information. This commit only encodes the signal into the response. |
||
|
|
35bf8dab73 |
Emit the static paths HMR update after updating the cache (#96019)
In dev, the server sends a `SERVER_COMPONENT_CHANGES` HMR update when `generateStaticParams` produces a different set of static paths, so the render picks up the new `fallbackParams` (added in #85741). It was emitting that update before writing the new result to `staticPathsCache`, so the refresh it triggered could read the previous, stale result and keep rendering with the old `fallbackParams` (there are `await`s for the prerender manifest between the two points, which widens the window). This moves the emit to after `staticPathsCache.set`, so the triggered refresh always observes the updated result. This also adds the end-to-end regression test that was missing for this behavior. Reusing the existing `use-cache-params/[slug]` fixture, it asserts the blocking-route redbox shown when a route reads a fallback (unknown) param outside a Suspense boundary, then adds a `generateStaticParams` covering the requested slug and asserts the redbox clears. Editing the page produces two refreshes: the one the edit itself triggers, which still renders with the stale `fallbackParams`, and the follow-up update this code emits once the new result is cached, which renders with the fresh ones. To ensure the test only passes because of the latter, `generateStaticParams` is given a deliberate delay so its recompute finishes only after the edit's own refresh has already re-rendered with the stale params. closes NAR-496 --- <sub>Stack created with <a href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub> |
||
|
|
18d2e2da5f |
Insights: use a single Learn more link in console errors (#95967)
## Summary
The instant / blocking-prerender insight console errors printed a docs
URL under each fix option (two or three anchors per message). This
collapses them to a single `Learn more:` link at the end of each
message, while keeping the `[stream]`/`[cache]`/`[block]` fix-option
labels. It restores the single-link format these builders originally
shipped with.
**Before:**
```
Ways to fix this:
- [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
https://nextjs.org/docs/messages/blocking-prerender-runtime#wrap-in-or-move-into-suspense
- [block] Set `export const instant = false` to allow a blocking route
https://nextjs.org/docs/messages/blocking-prerender-runtime#allow-blocking-route
```
**After:**
```
Ways to fix this:
- [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
- [block] Set `export const instant = false` to allow a blocking route
Learn more: https://nextjs.org/docs/messages/blocking-prerender-runtime
```
Covers all 16 insight-kind errors, across `blocking-route-messages.ts`,
`sync-io-messages.ts`, `dynamic-rendering-utils.ts`, and
`instant-messages.ts`.
Because the dev overlay classified these errors by the `#`-anchored docs
URL, `getBlockingRouteErrorDetails` is updated to match the anchor-less
`Learn more:` URL. Console-message snapshots and the guidance-data
extraction test are updated to the new format. The dev-overlay fix-card
data (`instant-guidance-data.ts`) keeps its per-card links, since those
are the overlay UI rather than the console message.
## Verification
- `pnpm --filter=next types`
- Snapshots regenerated with `jest -u` for the affected suites
<!-- NEXT_JS_LLM -->
|
||
|
|
4df37b6eed |
[ci] split up large cache-components-errors tests (#95623)
See https://github.com/vercel/next.js/pull/95553 for an explanation of why we want to split these up. TL;DR it makes CI faster and saves us some money. In CI we have to run tests at the suite/file granularity, so if a test suite is too long it limits parallelism and if it has to be retried, we have to retry the full suite. |
||
|
|
d6594bd111 |
Split typeof-window server requires into .browser variants (#95201)
First batch of fixes that addresses most of the recent bundle size regressions. |
||
|
|
41114a31b8 |
Remove 'silence this warning' from instant validation fix output (#95187)
## What
Removes the `silence this warning` phrasing from the structured fix
lines in instant validation output:
- `- [block] Set \`export const instant = false\` to ~~silence this
warning and~~ allow a blocking route`
- `- [ignore] Set \`export const instant = false\` to ~~silence this
warning and~~ opt the route out of instant-navigation validation`
- `- [ignore] Set \`export const instant = false\` to ~~silence this
warning and~~ opt the dropped segment out of instant-navigation
validation`
## Why
The line called itself a warning while being logged via `Error:`. This
PR originally renamed it to `silence this error`, but that has the
mirror problem: at validation level `warning` or `manual-warning` (or
when the check only surfaces in dev) nothing blocks, so "error"
over-claims.
The verb is also wrong either way. Setting `instant = false` doesn't
silence a problem that still exists. It declares that blocking is
acceptable for the route, so validation stops treating it as one.
Removing the clause leaves wording that is correct at every validation
level and matches the dev overlay cards ("Allow blocking route",
"Disable validation on this route") and the docs sections the lines link
to.
## How
- `blocking-route-messages.ts`, `dynamic-rendering-utils.ts`: drop
`silence this warning and` from the `[block]` lines
- `instant-messages.ts`: drop it from the `[ignore]` lines (unrendered
segment, link prefetch)
- `errors.json`: regenerated, append-only (codes 1394-1405)
- Storybook fixture and 15 test files updated to the new strings
<!-- NEXT_JS_LLM_PR -->
|
||
|
|
c131314bcf |
[PP] Validate Shell prefetches (except gSP) (#95151)
Implements instant validation for `partialPrefetching`. In this mode, `<Link>` prefetches an App Shell, which cannot access link data, and we need to warn for that. The changes in `instant-validation.tsx` are relatively simple: for an App Shell, we simply use `ShellRuntime` for all the new segments. We might also force them into `Runtime` for the purposes of discriminating dynamic holes. If a hole is present in `ShellRuntime` but disappears in `Runtime`, then we know it's caused by **link data** (as opposed to runtime or dynamic data). I've added some new error messages for this case. Note that the implementation here is incomplete: it uses the chunks from the dev render, which resolves static params in the `Static` stage. We use `ShellRuntime` for validating the App Shell, so as a result, static params are incorrectly included in it and don't trigger link data errors. This will be implemented in a follow-up. Note: It seems like we have some pre-existing bug in build validation where `fallbackParams` aren't populated, so params resolve statically when they shouldn't. I've marked two tests with `// TODO(app-shells): missing fallback params in build validation` so we can follow up and fix those. |
||
|
|
a4c56f5d4e |
log config evaluation time (#94811)
Log how long it takes to evaluate next.configs Sometimes it is very slow this can help users understand at least that it is due to their config Example dev log ``` running pnpm next --turbopack────────────────────────────────────────────────────────────────╯ ▲ Next.js 16.3.0-canary.52 (Turbopack) - Local: http://localhost:61086 - Network: http://10.103.12.203:61086 ✓ Ready in 272ms ✓ Running next.config.js took 13ms ``` Example build log ``` ✓ Running next.config.js took 10ms ▲ Next.js 16.3.0-canary.52 (Turbopack) - Experiments (use with caution): ✓ mdxRs ⚠ The "middleware" file convention is deprecated. Please use "proxy" instead. To migrate automatically, run: npx @next/codemod@canary middleware-to-proxy . Learn more: https://nextjs.org/docs/messages/middleware-to-proxy Creating an optimized production build ... ``` A few things to discuss * the bundler logging is incorrect in dev! If you use the `withRspack` plugin it will change the bundler to rspack but the next.js version string was already printed! * we eval config before printing the version in build but afterwards in dev so the evaluation timing line is a little surprising * this demonstrates that we do evaluate the config after 'Ready in' in dev, fine but maybe confusing for people? |
||
|
|
b429b5b69c |
Increase the code frame width used when logging to a file. (#95283)
### What? Bump the default code frame width (used when stdout isn't a terminal) from 100 to 240 columns. ### Why? When code frames are captured to a log file or build container, there's no terminal width to read, so we fall back to a default. 100 columns is too narrow for log output — build-log viewers soft-wrap or scroll, so the cost of wrapping too narrow (scattering the caret line away from the code) is worse than being a bit wide. 240 fits nearly all hand-written source while still bounding per-frame output so a minified/generated line can't dump kilobytes into the log. <!-- NEXT_JS_LLM_PR --> |
||
|
|
3767dfae1f |
test: fork select tests on partialPrefetching (#95279)
Stopgap until we make this a proper part of the CI matrix |
||
|
|
5ed0a5c988 |
dev-overlay: wire Link prefetch={true} Partial Prefetching warning into Insights (#94798)
### What?
Wires the dev-only `<Link prefetch={true}>` Partial Prefetching warning
from [#94672](https://github.com/vercel/next.js/pull/94672) into the
Instant Insights surface. Code frame, call stack, three fix cards:
**Upgrade** `prefetch = 'partial'` · **Disable** the prop · **Ignore**
with `instant = false`.
Demo:
[93-link-prefetch-without-partial](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/93-link-prefetch-without-partial).
### How?
- Warning factored into a shared `instant-messages.ts` factory.
- New `link-prefetch-partial` overlay kind + `Instant` label +
`InstantRuntimeError` dispatcher.
- Two new fix-card groups: **Upgrade** (amber, arrow-up), **Disable**
(gray, minus).
- New `instant-link-prefetch-partial.mdx` rule page + "Auditing existing
calls" section in the adoption guide.
- Unit tests for the matcher, cards, and `isInstantNavigationError`.
### Related
- [#94818](https://github.com/vercel/next.js/pull/94818) — broader
Partial Prefetching docs cleanup. Lands independently.
- [vercel/front#73592](https://github.com/vercel/front/pull/73592) —
next-site `upgrade` + `disable` `FixGroup` values.
|
||
|
|
6cc1049d4e | Revert "Remove legacy PPR codepaths" (#95113) | ||
|
|
f965c00411 |
Insights: drop irrelevant fix cards from instant errors (#94926)
### Why? Two Insight fix cards were misleading: 1. **`generateStaticParams` showed up on every runtime/client-hook insight.** One error covers `cookies()` / `headers()` / `params` / `searchParams`. GSP only applies to `params`; for the others it's noise. Even for `params` it nudges devs to make the route static instead of fixing the immediate error. 2. **`"use cache"` showed up on `connection()` triggers.** Caching `connection()` is contradictory. Both manifest on initial load and in-navigation (the fix-card sets are shared). ### What? 1. **Drop the GSP card** from runtime + client-hook sets. Affects [01-cookies-body](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/01-cookies-body), [03-params-body](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/03-params-body), [90-client-use-params](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/90-client-use-params), [41-subnav-cookies](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/41-subnav-cookies), [42-subnav-fetch](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/42-subnav-fetch). 2. **Filter `"use cache"`** when the cause is `connection()`. Affects [05-connection-body](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/05-connection-body), [08-connection-body-dynamic](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/08-connection-body-dynamic), [31-connection-in-metadata](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/31-connection-in-metadata), [33-connection-in-viewport](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/33-connection-in-viewport). [06-uncached-fetch-body](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/06-uncached-fetch-body) keeps the Cache card. ### How? - `getCards()` filters the cache card when `cause === 'connection'`. - New `deriveCauseFromCodeFrame()` helper detects `connection(` on the highlighted code-frame line. - `ParamClientHookDynamicError` collapsed into `ClientHookDynamicError`. - CLI/build messages: dropped the GSP bullet; added `(does not apply to \`connection()\`)` on the cache bullet. - Docs: removed `For known params, prerender` sections; added a connection caveat on dynamic/metadata-dynamic/viewport-dynamic pages. <!-- NEXT_JS_LLM_PR --> |
||
|
|
e7f4e336c0 | Remove legacy PPR codepaths (#94955) | ||
|
|
89c6799257 |
Statically prerender metadata image routes under Cache Components (#94957)
Under Cache Components, metadata image routes such as `opengraph-image` and `icon` that return an `ImageResponse` were always rendered on demand (`ƒ`) rather than prerendered. `ImageResponse` defers rasterizing its element tree into the response body stream, and the route-handler prerender unwraps that body within a single task; the rasterization never finishes within that budget, so the route was classified as dynamic. This change renders and caches the image during the prerender so these routes become static (`○`). During the prerender we serialize the `ImageResponse` arguments with React Flight's `prerenderToNodeStream` inside the prerender work-unit store, which runs the user's component tree once in the correct scope. This brings any user-space I/O inside that tree, such as `cookies()` or an uncached `fetch`, under the same Cache Components rules that already governed I/O elsewhere in the handler. The hanging-input abort signal bounds the serialization and decides static versus dynamic: if the tree is still waiting on dynamic input once the prerender's cache-sourced input is ready, the serialization can't complete and we return a hanging promise, so the final prerender's macrotask budget classifies the route as dynamic; a tree that resolves entirely from static data or `use cache` finishes serializing and is rendered to an image. This mirrors `encryptActionBoundArgs`, which serializes server action bound args with React Flight under the same hanging-input abort signal during a prerender. The fully resolved element tree is then handed to satori. Because React Flight encodes an async Server Component's output as a `React.lazy` that satori can't walk, those references are resolved into plain elements first; this lets an async server component, including one that uses `use cache`, be passed as the `ImageResponse` element. Rasterization runs outside the prerender work-unit store: inside a Cache Components prerender an uncached `fetch`, such as the renderer loading a font, is turned into a hanging promise (Cache Components skips I/O that would not be cached anyway), so running satori with no store lets those framework fetches resolve normally. Crucially, because satori only walks the already-resolved tree, no user component runs in that storeless scope, so uncached user-space I/O can't be wrongly allowed there and let a route that should be dynamic render as static. The rendered image is stored as an `ArrayBuffer` in a new in-memory `imageResponses` store on the Resume Data Cache, keyed by a base64 encoding of its serialized arguments, and the cache signal is held open until it is stored so the prospective prerender waits for it. The final prerender retrieves the array buffer from memory within microtasks. The store is in-memory only and is never serialized, so the image array buffers never enter the resume data that ships with the prerender. The caching path lives in a separate `cache-image-response` module that is loaded only for Cache Components builds, gated behind `process.env.__NEXT_CACHE_COMPONENTS` so the `require` and its React Flight dependencies are eliminated as dead code otherwise; apps without Cache Components keep `ImageResponse`'s original streaming behavior unchanged. --------- Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> |
||
|
|
f90e8bd21a |
Upgrade React from d9158919-20260615 to ad78e251-20260616 (#94867)
Co-authored-by: next-js-bot[bot] <279046576+next-js-bot[bot]@users.noreply.github.com> Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com> |
||
|
|
d424aa8ec4 |
Stabilize unstable_instant (#94578)
This API has experimental modes for build validation but the dev validation is going to be stable in the next release. |
||
|
|
5b99d26df8 |
instant: polish client-hook overlay wording, cards, and docs links (#94496)
### What? Polishes the dev-overlay UX for the client-hook prerender error after Josh's framework fix landed in [#94494](https://github.com/vercel/next.js/pull/94494). The overlay now names the hook in the headline and shows per-hook fix cards. ### Why? Different hooks need different fixes. `useSearchParams` always suspends, but `generateStaticParams` doesn't apply to it. `useParams` is the only hook GSP resolves at build time. `usePathname` and `useSelectedLayoutSegment(s)` need a Suspense boundary or the `[block]` export. ### How? - Headline now reads "Next.js encountered URL data `useX()` in a Client Component outside of `<Suspense>`", matching the body factory wording. - Per-hook card sets in `instant-guidance-data.ts`: `useSearchParams` → Stream + Block; `useParams` → Stream + GSP + Block; `usePathname` / `useSelectedLayoutSegment(s)` → Stream + Block. - Build-time message in `ClientHookDynamicError` / `ParamClientHookDynamicError` matches the overlay card set. - Companion docs page: [vercel/front#72622](https://github.com/vercel/front/pull/72622). ### Verification Demo scenarios on the [error-messages-overhaul test app](https://error-messages-overhaul-ibsl.labs.vercel.dev/): [88-client-use-pathname](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/88-client-use-pathname), [89-client-use-search-params](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/89-client-use-search-params), [90-client-use-params](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/90-client-use-params), [91-client-use-selected-layout-segment](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/91-client-use-selected-layout-segment), [92-client-use-selected-layout-segments](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/92-client-use-selected-layout-segments). <!-- NEXT_JS_LLM_PR --> --------- Co-authored-by: Josh Story <gnoff@storyposted.com> |
||
|
|
6f4d94ac88 |
Stream Cache Components dev render instead of restarting on cache miss (#94457)
When Cache Components is enabled, `next dev` previously simulated a
production loading experience on every cold request. The render did a
prospective pass to detect cache misses, and on any miss it waited for
every cache to fill via `cacheSignal.cacheReady()` and then restarted
the render with warm caches before streaming anything, so the browser
saw nothing until the slowest cache had filled. Every cold load blocked
on cache population.
This change replaces the restart-on-cache-miss flow with a single
non-abandoning staged render that streams immediately and fills caches
as a side effect. On a cold load the Suspense fallbacks stream right
away and the cached content resolves as its cache fills; on a warm
reload the staged progression matches the previous no-cache-miss path.
The render is split into clearly owned pieces: `setUpStagedDevRender`
builds the staged controller, cache signal, and resume cache;
`streamStagedRenderInDev{Node,Web}` runs the streaming render and
reports a result once the stream has fully finished; and
`stagedRenderWithCachesInDev{Node,Web}` returns the stream and leaves
the validation follow-up detached so it never blocks the response.
The render advances its stages in sequential tasks, and the stream is
not handed back the instant it exists: it is held until the render has
advanced through the stage whose content belongs in the shell. That is
the static stage for initial loads, HMR refreshes, and plain
navigations, or the runtime stage for client navigations to a route with
a runtime prefetch config, whose runtime-prefetchable content the
navigation's prefetch would have settled. It never waits for the dynamic
stage. Buffering the shell before the first flush keeps the streaming
renderer from emitting a premature Suspense fallback for content that
belongs in the shell, and it mirrors production, where the static shell
(plus runtime-prefetchable content where configured) is served and the
remaining holes stream in as fallbacks.
Two internal reads that would otherwise register as synchronous IO and
wrongly force the render to the dynamic stage, the cache handler's
tag-expiry clock check and the hot reloader's module-scope dev client
id, are now read untracked: via `performance.timeOrigin +
performance.now()` like the `'use cache'` handler, and only in the
browser where the HMR connection reads it, respectively.
Cache Components rules validation now runs in that background follow-up,
once the streamed render has fully settled. `planDevValidation` inspects
the finished render and picks one of three paths: forward an invalid
dynamic usage error the streamed render already recorded and stop (for
example a request API used inside `'use cache'`); validate the streamed
render's own chunks when it neither missed caches nor hit sync IO; or,
when it did either, validate a dedicated warm-cache render instead.
Because that warm render reads the filled caches back rather than
filling them, it can surface an invalid dynamic usage error the cold
streamed render cannot, such as a nested dynamic `use cache` cache life
that propagated to a parent with no explicit `cacheLife`; that error is
forwarded and validation is skipped, just as one recorded by the
streamed render is.
Since cold loads no longer block on cache fills, the transient
cache-status indicator that reflected that wait is no longer emitted; a
follow-up will instead add an indicator that tells the user whether a
render streamed with cache misses, and so wasn't representative of
production.
|
||
|
|
2df0562f32 |
Specialize client hook prerender abort reasons (#94494)
Specialize Cache Components prerender abort errors for `useParams`, `usePathname`, `useSearchParams`, `useSelectedLayoutSegment`, and `useSelectedLayoutSegments` instead of reporting a generic abort reason. React now allows errors observed after abort begins but before the final abort task runs to replace the generic abort reason. Client hook promises can use this window to report which hook blocked prerendering while preserving higher-priority synchronous I/O errors. |
||
|
|
84f9247617 |
Upgrade React from f0dfee38-20260529 to 43bcbf80-20260603 (#94440)
[diff facebook/react@f0dfee38...43bcbf80](https://github.com/facebook/react/compare/f0dfee38...43bcbf80) <details> <summary>React upstream changes</summary> - https://github.com/facebook/react/pull/36586 - https://github.com/facebook/react/pull/36603 - https://github.com/facebook/react/pull/36585 - https://github.com/facebook/react/pull/36580 - https://github.com/facebook/react/pull/36584 - https://github.com/facebook/react/pull/36583 - https://github.com/facebook/react/pull/36576 - https://github.com/facebook/react/pull/36575 - https://github.com/facebook/react/pull/36574 </details> --------- Co-authored-by: next-js-bot[bot] <279046576+next-js-bot[bot]@users.noreply.github.com> Co-authored-by: Josh Story <gnoff@storyposted.com> |
||
|
|
7644b7e509 | Ensure aborting has the same inputs as render (#94436) | ||
|
|
83c375edb1 |
instant: prompts on all fix cards, [group]-tagged CLI bullets, new docs slugs (#94017)
### What? Adds "Copy prompt" button to all 33 instant-guidance fix cards. Updates card links, factory `Learn more:` URLs, and overlay routing to the new docs slugs. Adds `[group]` tag prefix to CLI fix bullets so agents can map them back to card prompts. ### Why? Cards tell developers _what_ to do. The button gives agents a ready-to-paste instruction. The `[group]` tag lets agents reading CLI output find the matching card in the docs without parsing prose. ### How? - `prompt` field on all 33 `FixCard` entries. - Button replaces the external-link icon in the top-right; link moves next to the label. - Card links updated to `blocking-prerender-*` and `instant-unrendered-segment` slugs (avoids overriding upstream pages). - Variant-aware URL routing for `metadata` and `viewport` (matches existing `blocking-route` pattern). `InstantHeaderExplanation` takes a `variant` prop. - Fix bullets prefixed with their card group: `[cache]`, `[stream]`, `[block]`, etc. Tags match `<FixOption group>` in the MDX docs. - CLI bullets use `unstable_instant = false` (the current API). Overlay cards keep `instant` (aspirational). - Metadata dynamic-marker bullet now mentions the Suspense wrapper. - Merged canary: unrendered-segment errors land in the Insights tab via `isInstantNavigationError`. ### Depends on - [vercel/front#71640](https://github.com/vercel/front/pull/71640) — 6 sync-IO pages - [vercel/front#71781](https://github.com/vercel/front/pull/71781) — 4 metadata/viewport pages + `instant-unrendered-segment` |
||
|
|
a0c6a6140e |
instant: enable navigation validation by default (#94312)
### What?
Flips the default `experimental.instantInsights.validationLevel` from
`'manual-warning'` to `'warning'` so Cache Components apps get
instant-navigation validation across all pages by default.
### Why?
`'manual-warning'` only validates pages that explicitly export
`unstable_instant`, so apps see nothing unless they opt in. `'warning'`
is what users have been turning on manually to actually see the feature
(`v0`, `vercel-site`).
### Test coverage
- Unit: `instant-config-normalization.test.ts` pins the framework
default at `'warning'`.
- Integration: new `instant-validation-level-default/` fixture (no
`instantInsights` in config) asserts implicit dev validation fires, and
that build is unaffected.
### Collateral test fixtures
Tests whose intent is unrelated to instant validation
(router-autoscroll, owner-stack, hmr-iframe, next-image,
server-source-maps, etc.) now hit new redboxes/console warnings because
their fixtures incidentally use dynamic data. Each opts out with
`experimental: { instantInsights: { validationLevel: 'manual-warning' }
}` in `next.config`.
### Open question
With no `'off'` / `'info'` tier today, silencing Insights after this
change requires setting `validationLevel: 'manual-warning'`. Is that
acceptable? Should we inform of this anywhere?
<!-- NEXT_JS_LLM_PR -->
|
||
|
|
79f9e67e23 |
Exclude static metadata and viewport from error recovery prerender when cache-components is enabled (#94134)
These functions can be dynamic however in this mode we are trying to recover on the client so there is little point in attempting to resolve the http error recovery metadata since it might block us producing a shell. We now exclude these from the errore recovery RSC payload since we will recover the metadata on hydration in the browser. |
||
|
|
0846789f74 |
Add Errors/Insights tab split to the instant error overlay (#94073)
### What?
Adds a tab bar to the dev overlay that separates normal errors
("Errors") from instant navigation errors ("Insights"). The indicator
pill also reflects the split.
### Why?
When `unstable_instant` validation produces navigation-phase errors
alongside regular prerender errors, they were mixed into a single list.
Developers had no way to tell which errors were structural
instant-validation issues versus regular runtime/prerender errors.
### Demo
- [Demo 1: prerender
blocking-route](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/01-cookies-body)
— `Blocking Route` badge (red), Errors tab.
- [Demo 2: navigation
blocking-route](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/42-subnav-fetch)
— `Instant` badge (amber), Insights tab.
### How?
- `Errors` splits `runtimeErrors` into `normalErrors` / `instantErrors`
(using the existing `inNavigation` flag from
`getBlockingRouteErrorDetails`) and defaults to whichever bucket has
errors.
- `ErrorTabBar` renders between the nav and dialog inside
`ErrorOverlayLayout` (new `tabBar` prop). Empty tabs are disabled.
- `ErrorOverlay` passes a `key` derived from the error composition so
tab state resets when the shape changes (e.g. normal errors resolve).
- `RenderErrorContext` gains `instantErrorCount`; the indicator pill
shows "N Issues", "N Insights", or "N Issues · N Insights" accordingly.
- Prerender errors show `Blocking Route` badge (red), navigation errors
show `Instant` badge (amber).
---------
Co-authored-by: Yavor Punchev <yavor.punchev@gmail.com>
|
||
|
|
0f35dab6e6 |
Improve notFound recovery in error pathway (#94037)
Effectively Relands #93988 Reverts #92231 Prerendering the notFound loaderTree turns out to not be the correct behavior in many cases. The original motivation of 92231 as to solve the connection closed problem caused by statically prerendering an incomplete HTTP event like notFound() when cacheComponents is enabled because it would leave holes in the inlined flight data that never resolved and led to reported errors on hydration. This change now continues to fork the CC behavior of the error handler but it treats the error page as a resumable page so fresh hydration data is generated if it wasn't able to be entirely static. This is good for correctness but it also means the page is not going to statically prerender the notFound UI. This will be handled in a followup --------- Co-authored-by: Hendrik Liebau <mail@hendrik-liebau.de> |
||
|
|
9741cb57a6 |
Revert "Prerender HTTP access fallbacks with Cache Components semantics" (#94018)
Reverts vercel/next.js#93988 This broke not-found handling for internal Vercel apps. |
||
|
|
5b6747ad3f |
Prerender HTTP access fallbacks with Cache Components semantics (#93988)
The body of `if (cacheComponents)` in `prerenderToStream` is extracted into a local helper, `prerenderWithCacheComponents(getPayload)`. The catch-block error paths now route through that helper whenever Cache Components is enabled, which means the legacy `prerender-legacy` store is no longer reachable from a Cache Components prerender. As a result, `notFound()`, `forbidden()`, and `unauthorized()` recovery renders the matching fallback boundary under `prerender` and `prerender-client` semantics. Dynamic API access in those boundaries — for example `useSearchParams()` without a surrounding `<Suspense>` — now surfaces as a blocking-route error instead of the legacy `BailoutToCSRError`. The dev side will be handled in a follow-up. The prerender handling diverged a bit from the dev rendering in #92231, and we'll likely need to re-align the dev rendering first. > [!TIP] > Best reviewed with hidden whitespace changes. |
||
|
|
fda6986b26 | [test] Prerendering HTTP access fallback pages with Cache Components (#93987) | ||
|
|
4e3eb5137e |
Polish instant fix cards and validation messages (#93894)
### What? - Rename cards to plain English: `Prerender params if known`, `Mark the route as dynamic`, `For telemetry, use a timing API`. - Remove `Wrap body in Suspense` card from viewport variants. - Body errors: `during the initial render` → `during prerendering`; `blocking navigation` → `blocking the page load`. - Server sync IO leads with `the unstable value <expression>`. - Client sync IO drops `fixed at build time`. - New loading-state icon for the `block` group. ### Demo - [Fix Overview](https://error-messages-overhaul-ibsl.labs.vercel.dev/fix-overview) <!-- NEXT_JS_LLM_PR --> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
27dbc903a0 |
Fix Date.now() cause shadowing in sync IO error overlay (#93857)
## What
When a user writes `Date()` or `new Date()` in a Server Component and
instant validation surfaces a sync IO error, the dev overlay incorrectly
labels the cause as `Date.now()`.
```tsx
return <p>{new Date().toString()}</p>
```
Overlay headline says: `Next.js encountered Date.now() without an
explicit rendering intent.`
## Why
`getBlockingRouteErrorDetails` in `errors.tsx` classifies the API by
scanning the error message with `String.prototype.includes` against
`SYNC_IO_APIS` in order, first match wins. The time-type factory always
embeds `Date.now()` in the `elapsedTimeBullet` text (`Measure elapsed
time with \`performance.now()\` instead of \`Date.now()\``) regardless
of which API the user called. Because `Date.now()` was first in the
array, it matched the bullet text and shadowed the real expression.
Fix: put `new Date()` and `Date()` before `Date.now()` so the
more-specific entries win when both appear in the message.
User-visible cards and docs URL are unaffected — all three time variants
map to the same card set — only the cause label was wrong.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|
|
7dc4cdd48c |
Show inner "use cache" as cause of nested-dynamic cache error (#93707)
When a `"use cache"` propagated a dynamic cache life (`revalidate: 0` or `expire` under 5 minutes) to a parent without an explicit `cacheLife`, the resulting error pointed only at the outer cache invocation. With the inner cache's call site missing, tracing which nested cache was responsible meant reading through the outer's body — fine when it's local code, much harder when the dynamism comes from a nested cache buried in a third-party dependency. This change attaches the inner invocation as `cause` of the error, so the dev redbox and the build log show two stacks: the outer that threw, and the inner that propagated the dynamic life. The inner call site has to be captured eagerly while `cache()` is still on the synchronous stack, because we only learn whether the inner resolved dynamic asynchronously — after `collectResult` finishes and `propagateCacheEntryMetadata` runs — and by then the inner's frames are no longer on the JS stack. We only construct the eager `Error` when the parent is itself a public `"use cache"` (the only case where this entry could become a propagated origin), so top-level caches skip the allocation. The eager `Error` is held on `cacheContext.dynamicNestedCacheError`; once propagation knows the inner resolved dynamic, it's copied onto the outer store's same-named field, then carried through the outer's own `collectResult` into its RDC entry — which the throw site finally reads back as `cause`. We keep the first dynamic child — the immediate origin from the throwing cache's perspective. The two nested-dynamic cache error messages also get a small cleanup: each used to write `"use cache"` two different ways within the same sentence (bare and backticked); both now write it the same way. <img width="2188" height="2662" alt="localhost_3000_use-cache-low-expire_nested (1)" src="https://github.com/user-attachments/assets/f5103e0e-b9c0-44c5-b93b-82981aa05219" /> |
||
|
|
f9278fdf92 |
Surface invalid dynamic usage errors via Flight in dev (#93706)
When a `'use cache'` recorded an invalid dynamic usage error on the work store (for example a `cookies()` call inside `'use cache'`, a nested-dynamic `cacheLife`, or a `'use cache'` fill timeout), `renderToHTMLOrFlight` used to throw the recorded error right after `renderToStream` returned. The throw bubbled up through `base-server` and ended up rendering the Pages-Router `/_error` page, which felt out of place in an app-router context. This change removes that throw, so the error reaches the dev overlay through the same Flight channel that already surfaces static-shell-validation and instant-validation errors — `logMessagesAndSendErrorsToBrowser`, called from `spawnStaticShellValidationInDev` and from the validation-skipped fallback in `generateDynamicFlightRenderResultWithStagesInDev`. The original motivation for the throw was to avoid double-logging in the uncaught case. Without it, both React's `serverComponentsErrorHandler` (which stamps a digest and emits a Flight error chunk) and `logMessagesAndSendErrorsToBrowser` would forward the same error. We now dedupe by skipping the `logMessagesAndSendErrorsToBrowser` call whenever the recorded error already carries a `digest`, since that is exactly the signal that React has already seen it. Caught cases (no `digest`) continue to surface through `logMessagesAndSendErrorsToBrowser` as a collapsed dev-overlay entry; uncaught cases surface via React's Flight error chunk as an auto-opened redbox. This also sets us up for the upstack PR that attaches the inner cache call site as `cause` of the nested-dynamic prerender error. With errors now travelling uniformly over Flight — which preserves `cause` natively — the cause flows straight through to the dev overlay without needing to serialize it into the error page. |
||
|
|
23ccbaae2f |
fix: renumber non-sequential errors in errors.json (#93824)
yesterday, PR #93399 introduced some non-sequential codes into `errors.json`, which seems to mess with `scripts/merge-errors-json`, the script responsible for automatically re-numbering error codes to fix conflicts. i can fix the script later but for now as a workaround let's just fix the error codes manually |
||
|
|
3cf7aa2473 |
Honor Suspense-above-body opt-in for dynamic generateViewport (#93759)
|
||
|
|
7f90cce674 |
Extend instant error overlay to metadata, viewport, and sync IO errors (#93287)
### What? Extends the card-based instant error overlay (#92638) to metadata, viewport, and sync IO errors, and updates the matching build/CLI messages to a consistent structured format. ### Why? After the blocking-route redesign, metadata, viewport, and sync IO errors still used the old prose format with no visual fix guidance. ### Demo - Metadata: [runtime](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/12-cookies-in-metadata) · [dynamic](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/13-fetch-in-metadata) - Viewport: [runtime](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/14-cookies-in-viewport) · [dynamic](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/15-fetch-in-viewport) - Sync IO: [Math.random()](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/38-math-random-no-instant) · [Date.now()](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/39-date-now-no-instant) · [crypto.randomUUID()](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/40-crypto-random-no-instant) - Sync IO in Client: [Math.random()](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/44-client-math-random-no-suspense) · [Date.now()](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/43-client-date-no-suspense) · [crypto.randomUUID()](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/45-client-crypto-no-suspense) - [Fix overview (all cards)](https://error-messages-overhaul-ibsl.labs.vercel.dev/fix-overview) ### How? **Overlay** - New `dynamic-metadata`, `dynamic-viewport`, and `sync-io` error types in `errors.tsx`, all rendered through `InstantRuntimeError` with kind-specific fix cards - `isSyncIOError()` detects sync IO via the docs URL pattern, mirroring `isRuntimeVariant()` **Build & CLI messages** - Headlines aligned to "Next.js encountered..." across all error families - `blocking-route-messages.ts`: metadata/viewport errors restructured from prose to "Ways to fix this:" bullets - Sync IO messages extracted into `sync-io-messages.ts`, mirroring `blocking-route-messages.ts`. Helpers renamed to `createSyncIOError` / `createSyncIORuntimeError` / `createSyncIOClientError` to match the `create*Error` pattern |
||
|
|
215a08e2da |
Disable instant validations in draft mode (#93472)
This PR makes dev-mode cache bypass behavior consistent when `draftMode` is enabled. Draft mode now skips Instant Insights validation the same way a hard refresh or DevTools “Disable Cache” request does, and the Next.js devtools badge shows the existing “Cache disabled” state for draft-mode previews. It also adds coverage for draft-mode cache bypass behavior in dev and start modes, plus a devtools badge test, and updates the `cache-bypass-in-dev` docs to explain why draft mode triggers this state. |
||
|
|
d2e6b6c0e4 |
Preserve __NEXT_ERROR_CODE across the /_error page handoff (#93183)
When SSR fails in development — whether the failing page is in the App Router or the Pages Router — Next.js falls back to rendering the Pages Router `/_error` page and serializes the original error into `__NEXT_DATA__.err`. The client bootstrap in `packages/next/src/client/ index.tsx` later re-throws a fresh `new Error(initialErr.message)` so that the dev overlay picks it up. The error-code SWC plugin stamps that new `Error` with the generic code mapped to `%s` in `errors.json` because the message argument is an identifier, not a statically-known string, which is how the overlay ended up showing the wrong code for errors like `UseCacheTimeoutError`. The pipeline has two problems on the way to the overlay. First, `errorToJSON` in `packages/next/src/server/render.tsx` only copies the error's standard fields (`name`, `source`, `message`, `stack`, `digest`) into `__NEXT_DATA__.err`, so the real `__NEXT_ERROR_CODE` attached to the thrown error never reaches the client. Second, the subsequent rewrap in `getServerError` at `packages/next/src/server/dev/node-stack- frames.ts` creates yet another `new Error(...)` that the plugin stamps with the same generic code, clobbering anything the caller might have set on the rewrapped instance. This change plumbs the code through. `errorToJSON` now also emits `__NEXT_ERROR_CODE` using `extractNextErrorCode` so the value survives JSON serialization into `__NEXT_DATA__.err`; the type in `packages/next/src/shared/lib/utils.ts` is updated to match. Both rewrap sites — the client bootstrap and `getServerError` — copy the code from the source error onto the fresh `Error` via `Object.defineProperty` with `enumerable: false` and `configurable: true`, matching how the plugin itself stores the property, which overrides the generic code the plugin stamped on the rewrapped instance. The `use-cache-hanging` e2e test snapshot is updated from `E394` to `E236` now that the real code surfaces in the dev overlay. |
||
|
|
e9bc6190bd |
Redesign blocking route dev overlay and build errors (#92638)
### What? Redesigns the blocking-route error overlay for instant navigation errors with a distinct "Instant" overlay path, visual technique cards, and updated error wording framed around navigation impact. ### Why? The current overlay dumps every possible cause and fix in one block of text. The new design is friendlier — amber "Instant" badge, a short headline framed around navigation, and responsive code snippet cards showing each fix pattern. ### Demo - **Runtime template** (e.g. `cookies()`): https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/26-cookies-ssr-no-instant - **Dynamic template** (e.g. uncached `fetch`): https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/27-fetch-ssr-no-instant ### How? **Overlay** - New early-return path in `errors.tsx` for `blocking-route` errors without refinement — renders `InstantRuntimeError` with CodeFrame → description → technique cards → CallStack → ErrorCause - `InstantGuidance` component with responsive CSS grid of fix technique cards (3 per variant) - Color-coded cards with colored borders and matching highlight text (blue, purple, red) - "Make route params static" card (runtime only) has a dashed border indicating it's conditional **Build & CLI messages** - Build output messages extracted into `blocking-route-messages.ts` and deduplicated across `dynamic-rendering.ts` - `dynamicOrRuntimeBodyMessage` added for build-time static validation where the specific cause can't be pinpointed — lists all APIs (`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, `connection()`) - `isRuntimeVariant()` replaces the old `includes('cookies()')` heuristic which broke because both templates mention `cookies()` - `logBuildDebugHint()` extracted and shared between `logDisallowedDynamicError` and instant validation — adds "run `next dev`" and "`next build --debug-prerender`" hints to instant validation build output Results: <img width="2094" height="1478" alt="Google Chrome 2026-04-17 16 37 00" src="https://github.com/user-attachments/assets/04f126c5-250c-4e6e-bad0-d6960496cd13" /> <img width="1978" height="1512" alt="Google Chrome 2026-04-17 16 36 37" src="https://github.com/user-attachments/assets/46a502d8-9500-4055-814b-3efb739949db" /> --------- Co-authored-by: Janka Uryga <lolzatu2@gmail.com> |
||
|
|
662c6d575f |
Warn for non-deterministic "use cache" args during final prerender (#92820)
When a `"use cache"` function receives arguments that differ between the cache warming phase and the final prerender, the cache key changes and the Resume Data Cache (RDC) entry from the prospective prerender is missed. This can happen for various reasons, for example when concurrent async operations push results into a shared array in non-deterministic order, and that array is then passed as an argument to a cached function. Without a `cacheSignal` to keep the render alive, the final prerender aborts the cache entry generation, producing an incomplete RSC stream that causes "Connection closed" errors. This change detects that scenario (an RDC miss during the final prerender where `cacheSignal` is `null`) and returns a hanging promise instead of generating a broken cache entry. A warning is logged to help developers identify the non-deterministic arguments. By making the cached function a dynamic hole rather than erroring, the prerender can still complete and produce at least a partial shell if there is a Suspense boundary above. This affects both on-demand prerendering and runtime prefetching. To avoid false positives, cache keys that were intentionally skipped during the prospective prerender (e.g. because the cached function accessed fallback params) are tracked in a `dynamicCacheKeys` set on the RDC. During the final prerender, a known dynamic key is returned as a hanging promise early without logging a warning. This also serves as a performance optimization, since it avoids trying to regenerate the entry. This set is intentionally not serialized, as cache misses for dynamic keys should generate fresh entries during the resume at request time. |