mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
codex/ledgers-react-sync
70 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 -->
|
||
|
|
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 --> |
||
|
|
390eff3b86 |
[ci] Share a single browser instance across all test suites in a single job (#95589)
The idea here is that we waste a bunch of CPU setting up each test suite by spawning an entirely new Chrome process, but playwright can share the same browser across many tests. Tests are still isolated at the browser level, just not at the OS process level. Hoping to see some performance improvement in e2e tests. Compare to: https://github.com/vercel/next.js/pull/95617 Somewhat unscientific (single sample) results: ``` Comparing the head-commit build-and-test runs — PR #95589 run [28981702219 ](https://github.com/vercel/next.js/actions/runs/28981702219)vs PR #95617 run [28981686692](https://github.com/vercel/next.js/actions/runs/28981686692). Both ran 101 jobs and both succeeded. ┌─────────────────────────┬────────────────┬───────────────────────┬───────────────────┐ │ Metric │ #95617 control │ #95589 shared browser │ Delta │ ├─────────────────────────┼────────────────┼───────────────────────┼───────────────────┤ │ Raw wall-time sum │ 616.2 min │ 571.2 min │ −45.0 min (−7.3%) │ ├─────────────────────────┼────────────────┼───────────────────────┼───────────────────┤ │ Billable (ceil per job) │ 658 min │ 617 min │ −41 min (−6.2%) │ └─────────────────────────┴────────────────┴───────────────────────┴───────────────────┘ The shared-browser experiment is cheaper, and the savings land almost exactly where you'd expect — the browser-driven test suites — while non-browser jobs (rust, lint, unit, windows) are flat within noise: ┌─────────────────────────────────┬─────────┬────────┬───────┐ │ Job category │ control │ shared │ delta │ ├─────────────────────────────────┼─────────┼────────┼───────┤ │ test prod │ 130.7 │ 110.5 │ −20.2 │ ├─────────────────────────────────┼─────────┼────────┼───────┤ │ cache components dev │ 51.5 │ 42.9 │ −8.7 │ ├─────────────────────────────────┼─────────┼────────┼───────┤ │ turbopack dev │ 75.1 │ 69.5 │ −5.5 │ ├─────────────────────────────────┼─────────┼────────┼───────┤ │ turbopack production │ 86.3 │ 81.1 │ −5.2 │ ├─────────────────────────────────┼─────────┼────────┼───────┤ │ test dev │ 110.4 │ 105.5 │ −4.9 │ ├─────────────────────────────────┼─────────┼────────┼───────┤ │ cache components prod │ 48.7 │ 49.0 │ +0.3 │ ├─────────────────────────────────┼─────────┼────────┼───────┤ │ firefox and safari │ 5.3 │ 7.3 │ +2.0 │ ├─────────────────────────────────┼─────────┼────────┼───────┤ │ flake-detection jobs (combined) │ ~15 │ ~21 │ +~6 │ └─────────────────────────────────┴─────────┴────────┴───────┘ Takeaways: - Net savings of ~41 billable minutes per run (~6%), concentrated in the Chromium-driven test prod/dev, turbopack, and cache components suites — consistent with reusing one browser process instead of spawning per-suite. - The small regressions are in firefox and safari and the "new/changed tests for flakes" jobs (+~8 min combined). Worth a glance, though they may just be run-to-run variance. Caveat: this is a single run per PR, so there's real variance run-to-run. That said, the fact that the deltas track the browser-heavy jobs specifically — and not the Rust/lint/unit jobs — is a good signal the effect is genuine rather than noise. If you want a firmer number, re-running each PR 2–3× and averaging would tighten it up. ``` |
||
|
|
a430936c7f |
Strengthen _rsc cache-busting param (#92755)
This updates `_rsc` cache-busting param generation to use a stronger digest so different RSC request variants are much less likely to produce the same value. It also updates the related request-validation and injected resume/instant-test paths so they continue to generate and validate the same `_rsc` values across client, server, and edge-reachable codepaths. |
||
|
|
c53f5863e3 |
[test] Unflake app prefetch test (#91734)
[Flakiness metric](https://app.datadoghq.com/ci/test/runs?query=test_level%3Atest%20%40git.repository.id%3A%22github.com%2Fvercel%2Fnext.js%22%20%40test.type%3A%22nextjs%22%20%40test.status%3A%22fail%22%20%40test.name%3A%22app%20dir%20-%20prefetching%20should%20not%20unintentionally%20modify%20the%20requested%20prefetch%20by%20escaping%20the%20uri%20encoded%20query%20params%22&agg_m=count&agg_m_source=base&agg_t=count&fromUser=false&index=citest&start=1773424617562&end=1774029417562&paused=false) Another victim of the Playwright upgrade. Adding a `retry` to the assertion should fix the flakiness, as it seems that the element is not always immediately visible after the click. |
||
|
|
df247d1b51 |
Move prefetch hints from CacheNodeSeedData to FlightRouterState (#90066)
## Summary
Prefetch hints were previously stored in CacheNodeSeedData — the payload
the server sends for each segment's React content. This moves them to
FlightRouterState, the route tree structure that the server sends
alongside the seed data. FlightRouterState is a better home because
prefetch hints describe properties of the route structure (loading
boundaries, instant configs) rather than properties of the rendered
content. It also ensures that hints are always available during
navigations, not just prefetches.
Since this changes the FlightRouterState tuple layout anyway, it also
folds the existing `isRootLayout` boolean and `HasLoadingBoundary` enum
into the same bitmask field at FlightRouterState[4].
## Test plan
- Existing unit tests updated for new format
(`flight-data-helpers.test.ts`, `types.test.ts`)
- MPA navigation test validates root layout change detection via bitmask
- `prefetch-true-instant` e2e test validates Link `prefetch={true}`
behavior with instant routes
|
||
|
|
d3de9bce9b |
Embed static sibling info in route tree (#88692)
This change adds infrastructure for a planned client-side optimization where the router can infer route structure from previously prefetched routes, avoiding redundant route tree requests. When the client prefetches /blog/post-1 and learns the route structure is /blog/[slug], it could potentially reuse that knowledge for /blog/post-2 without another server roundtrip. However, if a static route like /blog/featured exists at the same level, the client needs to know NOT to apply the dynamic template for that URL. staticSiblings provides this information. When a dynamic segment like [slug] has static siblings (e.g., "featured", "archive"), the server includes them in the prefetch response. The client can then check whether an incoming URL matches a known static sibling, and if so, recognize that the previously learned route structure does not apply. |
||
|
|
bbfd5fb29c | [test] Disable flaky prefetching.stale-times test (#86299) | ||
|
|
bfe2ab445b | [test] Deflake prefetching.stale-times (#85733) | ||
|
|
cb01c9ea8f |
[test] Separate act and assertions (#85508)
Moves the assertions out of `act` and removes redundant waiting for elements. Similar to https://github.com/vercel/next.js/pull/85417 |
||
|
|
721eb61551 | [test] Speed up prefetching suite (#85417) | ||
|
|
dd5e3ad611 |
[ci]: move router-act to top level testing utils (#84653)
We're starting to use this pattern in more places, so this moves `router-act` into our top level testing utils. |
||
|
|
9c053332cc |
[test]: convert prefetching tests to be segmentCache compatible (#84521)
Refactors some older prefetching tests to be compatible with `clientSegmentCache`. The old tests were too tightly coupled to router implementation details. Some of the ones that don't make sense to convert were removed (for ex, asserting on prefetch cache seeding behavior, or fetch priority of "high", as fetch priority is managed differently with the new router). This refactors the tests to leverage the `act` util from `segment-cache` test suites, and implemented the accordion behavior to more granularly control when prefetches are dispatched. I copied those implementations for now since we don't have a global util for it. |
||
|
|
ee284b8ba1 |
refactor: lowercase app router header values (#82169)
we define most of the app router headers with capital letters, like
this:
```ts
export const NEXT_ROUTER_PREFETCH_HEADER = 'Next-Router-Prefetch' as const
```
which means we have to keep lowercasing the header everywhere when
checking for its presence, because node and other intermediate layers
normalize all headers to lowercase:
```ts
if (req.headers[NEXT_ROUTER_PREFETCH_HEADER.toLowerCase()]) { ... }
```
this seems pretty pointless -- we can just define the headers in
lowercase form, and skip all those `toLowerCase()` calls.
(all the above sentences have been normalized to lowercase to match the
spirit of this pr)
|
||
|
|
5c25087d57 |
Fix validateRSCRequestHeaders incorrect redirect (#82119)
Found during dogfooding on Vercel that sometimes the RSC validation fails incorrectly. The root cause was that headers like `Next-Router-Prefetch` were absent from the header and transferred to request meta. Eventually we should improve how we retrieve these fields consistently. |
||
|
|
65e0b7b757 |
bugfix: static resources staleTime should be renewed once refetched (#81771)
When the `x-nextjs-staletime` header is sent from the server, the client router was discarding all of its staleTime heuristics (such as lastUsedTime and "reusable" cache entries). This caused an issue where after the static staleTime window expired and the prefetch data was renewed, the static staleTime itself was not being renewed. As a result, once the static staleTime expired, the client would continuously refetch the resource on every navigation, even though it had just been renewed. This removes the `x-nextjs-staleTime` logic from this heuristic because it was only intended to be considered for the new `clientSegmentCache` & cache components work |
||
|
|
45a5405de2 |
bugfix: propagate staleTime to seeded prefetch entry (#81263)
In #71280, we hooked up the server-side `staleTime` header to the client-side prefetch cache. This means that if the server responds with a staleTime value, the client router will use that rather than the previous heuristic when determining if a prefetch entry can be reused. However, there was missing functionality to set a proper staleTime value for the initially seeded prefetch entry (aka the first page visit). In the case of customizing staleTimes, this meant that navigations back to the original page would defer back to the old stale heuristic, which would differ from subsequent navigations that were properly seeded with a staleTime. Separately, I believe that it's problematic that if the `staleTime` header is present, that we honor that and only set a fresh/stale cache status, because the newer heuristic doesn't consider the `reusable` cache status. This should probably be flagged to the clientSegmentCache flag. However, to minimize the changes here, I've only addressed this particular case. |
||
|
|
6fd6ed1ea2 |
Check cache busting search params on all RSC requests (#80669)
For all RSC requests, we require the `_rsc` search param to match the hash of its corresponding RSC headers. In the case where the match fails, we respond with a redirect to the correct search param. This hash check only applies to `next start` mode for now. |
||
|
|
100881542a | [test] Use NEXT_TEST_CI when forking test in CI (#79354) | ||
|
|
b42b442ead |
Client router should discard stale prefetch entries for static pages (#79309)
When navigating to a static page that was previously prefetched, the client router should discard a stale prefetch entry, and fetch the page again, if the stale time has passed. The selected stale time is either the default value of 5 minutes, a custom value set via `experimental.staleTimes.static`, or a stale value set via a `cacheLife` profile when using `"use cache"`. This also fixes an issue where the client router interpreted the stale time value as milliseconds, whereas the server sends it in seconds. This was previously already fixed with `clientSegmentCache` enabled in #74759. fixes #74272 fixes #79093 Closes NEXT-4109 |
||
|
|
4282bc6f91 |
remove BrowserInterface (#78308)
rip. you served a purpose once, but now you're just getting in the way. this improves typesafety quite a bit, because `BrowserInterface` had a whole bunch of random `any`s everywhere also - removes `evalAsync`. no idea why that was needed, but we're happily using promises in normal eval, so it can be dropped - adds more safety to `chain` |
||
|
|
60eb702f45 |
fix: enhance a11y, prevent double firing in error rating (#74563)
Improved a11y (aria-live, aria-label) and simplified loading/error state to prevent double firing when feedback buttons get clicked multiple times while loading. |
||
|
|
bd96b91dbf | Fix: Preserve intentional percent encoding in search param for client nav (#74473) | ||
|
|
e08fe27ab7 |
Restore RSC fetch error handling after navigating back (#73985)
As a follow-up to #73975, we're now restoring the error handling for failed RSC fetch calls when the user navigates back after a hard navigation. In this case the browser uses the bfcache to restore the previous JavaScript execution context, and thus the abort controller will still be in the aborted state. To take this into account, we're now creating a new `AbortController` instance on `'pageshow'` events. In addition, the abort controller's `signal` is now actually passed to the `fetch` call, and not only used for the error handling, so that the requests are aborted on `'pagehide'`. This was an oversight in the original PR. With that, it's even more important to create a fresh abort controller, otherwise RSC fetching would be disabled after back navigation. The added e2e test can only run in headed mode unfortunately, as the bfcache is not available in headless mode. (Using the same approach as in #54081.) |
||
|
|
ac89400a2a |
Ignore RSC fetch errors after hard navigation (#73975)
When the user reloads the page or navigates away while RSC fetch requests are still in flight – most notably background prefetches – we currently log the following error: ``` Failed to fetch RSC payload for <URL>. Falling back to browser navigation. ``` This unnecessarily clutters the DevTools console and creates noise in remote error reporting tools. When the browser cancels a `fetch` request due to navigation, it throws `TypeError: Failed to fetch`. We shouldn't universally suppress this error since it may also occur for other valid reasons where we do want to log it. To suppress the error only in genuine navigation scenarios, we introduce an `AbortController` and pass its signal to the `fetch` call. On `'pagehide'` events, we abort the controller. This approach ensures that error log suppression is limited solely to these navigation-related cases. [x-ref](https://vercel.slack.com/archives/C0676QZBWKS/p1734009715952909) fixes #60549 |
||
|
|
1734e91ac8 |
fix loading bailout condition & server patch mismatch tolerance (#72227)
Pre-PPR on client navigations, we are currently determining whether or not to render the underlying React components based on the presence (or lack thereof) of `loading.js`. We'll render components up to the first segment containing `loading.js`, and then stop at that level. Similarly, if there's no `loading.js` in the tree at all, we'll return a mostly empty prefetch. The underlying reason for this is that we cannot assume a particular tree/subtree doesn't contain expensive invocations: `loading.js` is a signal we use to detect where some sort of dynamic call will happen. This heuristic is largely improved with PPR + DIO but we still need to support this case. Currently there's a bug that will lead to overfetching. When determining if we should skip the component tree, we check the presence of loading at that level, as well as the entire tree. This means that during partial navigations when the server starts rendering deeper within the tree rather than starting from the root, the presence of `loading.js` in an earlier segment would incorrectly tell the router it should render the page. This updates the handling to check for a loading segment only within the partial tree that's being diffed. That way when we recursively call this, it won't look at earlier segments when deciding if there's a loading segment deeper in the tree. This PR also adjusts the server-patch reducer to be more resilient to mismatching trees. Previously we triggered a hard navigation but this actually can become a fairly common case if the RSC response doesn't return initial bytes quickly enough, such as in `dynamicIO` when we're warming the dev caches prior to a navigation. Instead we'll return the state in-place and effectively discard the patch because the assumption is the tree was patched elsewhere. |
||
|
|
f91cdec44d | test: migrate rest async api usage in tests (#71663) | ||
|
|
98ffc0e179 |
initially visited static pages should respect static staletime (#70640)
This provides information to the initial SSR render that the client leverages when seeding the prefetch cache, to determine if a page was a fully static prerender. The side effect of not doing this is that if we seeded a static page during SSR, and then navigated to it, the router would make a request for data that it didn't need. In other words, it didn't honor the static staletime, like other navigations would. |
||
|
|
0249bdf3c1 |
bugfix: only replace with full prefetch if existing data was partial (#70650)
By default we are storing the seeded prefetch entry (from the SSR
render) as an "auto" prefetch. The prefetch cache is used because every
navigation checks against the prefetch cache to determine if it has the
necessary data to render the requested segment(s).
We have a router heuristic that will re-fetch a page that's already in
the prefetch cache if the one that is stored is an "auto" prefetch, and
the newly requested prefetch is a "full" prefetch. This is because the
assumption is the "auto" prefetch would contain partial data
(potentially only `loading.js` data) while the "full" prefetch has
everything, so we'd want to replace the cache entry with the most up to
date information.
Currently when we seed the prefetch cache with the initially SSRed page,
we set a cache status of "auto". This is because a "full" prefetch has
staleTime implications: a full prefetch will be client cached for 5
minutes by default (the `static` staleTime), and since we have no
prefetch intent during SSR, we should fallback to the automatic caching
heuristics to avoid caching a page longer than what was intended.
However, this will trigger the above mentioned logic to switch to a
"full" prefetch if the page you're currently on has a link to itself,
with `prefetch={true}`, resulting in a wasted request.
This refactors the logic responsible for switching a prefetch entry to a
"full" prefetch only once we confirm the response payload from the
existing cache entry wasn't a response that started rendering from the
root. If we started rendering from the root, we know that we have the
full data already, so it'd be wasteful to fetch again.
Fixes #70535
|
||
|
|
05f159dffc |
[Breaking] Update Dynamic APIs to be async (#68812)
Next.js has a number of dynamic APIs that aren't available during
prerendering. What happens when you access these APIs might differ
depending on the mode you are using, for instance if you have PPR turned
on or the newly introduced `dynamicIO` experimental mode. But regardless
of the mode the underlying API represents accessing something that might
only be available at render time (dynamic rendering) rather than
prerender time (build and revalidate rendering)
Unfortunately our current dynamic APIs make certain kinds of modeling
tricky because they are all synchronous. For instance if we wanted to
add a feature to Next.js where we started a dynamic render before a
Request even hits the server it would be interesting to be able to start
working on everything that does not rely on any dynamic data and then
once a real Request arrives we can continue the render and provide the
associated Request context through our dynamic APIs.
If our dynamic APIs were all async we could build something like this
because they represnt a value that will eventually resolve to some
Request value. This PR updates most existing dynamic APIs to be async
rather than sync. This is a breaking change and will need to be paired
with codemods to realistically adopt. Additionally since this change is
so invasive I have implemented it in a way to maximize backward
compatibility by still allowing most synchronous access. The combination
of codemods, typescript updates, and backward compat functionality
should make it possible for projects to upgrade to the latest version
with minimal effort and then follow up with a complete conversion over
time.
#### `cookies()`
`cookies()` now returns `Promise<ReadonlyRequestCookies>`. Synchronous
access to the underlying RequestCookies object is still supported to
facilitate migration.
```tsx
// ------------ preferred usage
// async Server Component
const token = (await cookies()).get('token')
// sync Server Component
import { use } from 'react'
//...
const token = use(cookies()).get('token')
// ------------ temporarily allowed usage
// javascript, dev warning at runtime
const token = cookies().get('token')
// typescript, dev warning at runtime
import { type UnsafeUnwrappedCookies } from 'next/headers'
// ...
const token = (cookies() as unknown as UnsafeUnwrappedCookies).get('token')
```
#### `headers()`
`headers()` now returns `Promise<ReadonlyHeaders>`. Synchronous access
to the underlying Headers object is still supported to facilitate
migration.
```tsx
// ------------ preferred usage
// async Server Component
const header = (await headers()).get('x-foo')
// sync Server Component
import { use } from 'react'
//...
const header = use(headers()).get('x-foo')
// ------------ temporarily allowed usage
// javascript, dev warning at runtime
const header = headers().get('x-foo')
// typescript, dev warning at runtime
import { type UnsafeUnwrappedHeaders } from 'next/headers'
// ...
const header = (headers() as unknown as UnsafeUnwrappedHeaders).get('x-foo')
```
#### `draftMode()`
`draftMode()` now returns `Promise<DraftMode>`. Synchronous access to
the underlying DraftMode object is still supported to facilitate
migration.
```tsx
// ------------ preferred usage
// async Server Component
if ((await draftMode()).isEnabled) { ... }
// sync Server Component
import { use } from 'react'
//...
if (use(draftMode()).isEnabled) { ... }
// ------------ temporarily allowed usage
// javascript, dev warning at runtime
if (draftMode().isEnabled) { ... }
// typescript, dev warning at runtime
import { type UnsafeUnwrappedDraftMode} from 'next/headers'
// ...
if ((draftMode() as unknown as UnsafeUnwrappedDraftMode).isEnabled) { ... }
```
#### `searchParams`
`searchParams` is now a `Promise<{...}>`. Synchronous access to the
underlying search params is still supported to facilitate migration.
```tsx
// ------------ preferred usage
// async Page Component
export default async function Page({
searchParams
}: {
searchParams: Promise<{ foo: string }>
}) {
const fooSearchParam = (await searchParams).foo
}
// sync Page Component
import { use } from 'react'
export default function Page({
searchParams
}: {
searchParams: Promise<{ foo: string }>
}) {
const fooSearchParam = use(searchParams).foo
}
// ------------ temporarily allowed usage
// javascript, dev warning at runtime
export default async function Page({ searchParams}) {
const fooSearchParam = searchParams.foo
}
// typescript, dev warning at runtime
import { type UnsafeUnwrappedSearchParams } from 'next/server'
export default async function Page({
searchParams
}: {
searchParams: Promise<{ foo: string }>
}) {
const syncSearchParams = (searchParams as unknown as UnsafeUnwrappedSearchParams<typeof searchParams>)
const fooSearchParam = syncSearchParams.foo
}
```
#### `params`
`params` is now a `Promise<{...}>`. Synchronous access to the underlying
params is still supported to facilitate migration. It should be noted
that while params are not usually dynamic there are certain modes where
they can be such as fallback prerenders for PPR.
```tsx
// ------------ preferred usage
// async Segment Component
export default async function Layout({
params
}: {
params: Promise<{ foo: string }>
}) {
const fooParam = (await params).foo
}
// sync Segment Component
import { use } from 'react'
export default function Layout({
params
}: {
params: Promise<{ foo: string }>
}) {
const fooParam = use(params).foo
}
// ------------ temporarily allowed usage
// javascript, dev warning at runtime
export default async function Layout({ params}) {
const fooParam = params.foo
}
// typescript, dev warning at runtime
import { type UnsafeUnwrappedParams } from 'next/headers'
export default async function Layout({
params
}: {
params: Promise<{ foo: string }>
}) {
const syncParams = (params as unknown as UnsafeUnwrappedParams<typeof params>)
const fooSearchParam = syncParams.foo
}
```
### Typescript Changes
When using typescript with Next.js currently it is up to you to author
types for Pages, Layouts and other Segment components that recieve props
like `params` and `searchParams`.
Next comes with some build-time type checking to ensure you have not
improperly typed various top level module exports however the current
type assertions for `params` and `searchParams` is `any`. This isn't
very helpful because it allows you to erroneously type these props.
`searchParams` is tricky because while the default type is a dictionary
object parsed using node.js url parsing it is possible to customize when
running a custom Next.js server. However we can ensure that you
correctly type the prop as a Promise so with this change the validated
type for `searchParams` will be `Promise<any>`.
In the long run we will look at updating the `searchParams` underlying
type to be URLSearchParams so we can move away from supporting
customized parsing during rendering and we can get even more explicit
about valid types.
`params` is more straight forward because the framework controls the
actual `params` prop implementation and no customization is possible. In
the long run we want to enforce you are only typing params that are
valid for the Layout level your file is located in but for now we are
updating the allowed type to be `Promise<{[key: string]: string |
string[] | undefined }>`.
These new type restrictions may also require fixes before being able to
successfully build a project that updates to include these breaking
changes. These changes will also not always be codemodable because it is
valid to type the entire component using an opaque type like `Props`
which our codemods may not have an ability to introspect or modify.
|
||
|
|
cc92654396 |
fix app router prefetch deduping (#68642)
The `Link` component has built in prefetch deduping behavior that was added for pages router, since pages only supports a single kind of prefetch. Both pages & app routers share the `Link` component, meaning the deduping behavior was enabled for both. However, app router supports different [prefetch types](https://nextjs.org/docs/app/api-reference/components/link#prefetch). This means that if a page contains a link to a URL with auto prefetching, followed by a URL with `prefetch={true}`, the full prefetch would not be applied because it was short-circuited by `Link`. Closes NDX-124 |
||
|
|
8daa59eb7a |
prerendered pages should use static staleTime (#67868)
### What
Prefetches for static pages currently aren't distinguished from dynamic
pages insofar as their client router cache TTL is concerned. This meant
pre v15, both static and dynamic pages leveraged the 30s cache. In v15,
this meant it would refetch the static data every navigation, since we
changed the `dynamic` staleTime value to `0`. The only way to force a
prefetch to use the static staletime was via `prefetch={true}` on a link
(or `router.prefetch`).
### Why
The router had no way of distinguishing between static or dynamic
responses. The only difference was the RSC payload between the two,
since a static response would contain the full tree, and a dynamic
response would potentially be short-circuited with `loading.js` or have
a fairly minimal response for just basic metadata.
### How
This introduces a header the client can leverage to determine if the
response is a prerender. If it is, we mark the "auto" prefetch entry as
"full" so that it respects the `static` staleTime value (current default
is 5min).
We use a new header, rather than piggybacking on `x-nextjs-cache` /
`x-vercel-cache`, because a static page can technically have a `MISS`
status if it's revalidating.
Fixes #66513
Closes NDX-65
|
||
|
|
2fbdac6f82 |
fix prefetch bailout detection for nested loading segments (#67358)
### What When PPR is off, app router prefetches will render the component tree up until it encounters a `loading` segment, at which point it'll just return some metadata about the segment and won't do any further rendering. This is an optimization to ensure prefetches are lightweight and don't potentially invoke expensive dynamic subtrees. However, there's a bug in this logic that is causing it to bail unexpectedly if a segment deeper in the tree contained a `loading.js` file. This would mean the loading state wouldn't be triggered until the second request for the full RSC data is initiated, resulting in an unintended delta between when a link is clicked and the loading state is shown. ### Why The `shouldSkipComponentTree` flag was incorrectly being set to `true` even if the `loading.js` segment appeared deeper in the tree. Prefetch requests from the client will always contain `FlightRouterState`, so the logic to check for loading deeper in the tree will always be missed. ### How This removes the `flightRouterState` check as it doesn't make sense: prefetches will currently _always_ include the `flightRouterState`, causing this to always short-circuit. I believe that this check is vestigial from when we were generating static `prefetch.rsc` outputs which is no longer the case. This mirrors a [similar check](https://github.com/vercel/next.js/blob/b87d8fc49983a3be568517d7ae14087749bb8ce3/packages/next/src/server/app-render/create-component-tree.tsx#L393) when determining if parallel route(s) should be rendered. |
||
|
|
aa832c3b2d |
use fetch priority for app prefetches (#67356)
This leverages [fetch priority](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request#priority) to ensure automatic prefetching as a result of visiting a page is sent with "low" priority, to signal to the browser it can prioritize more important work necessary for rendering the page. A "temporary" prefetch (ie one that is created when there wasn't an existing prefetch cache entry on navigation) will use a "high" priority because it's critical to the navigation event. All other cases will be "auto" which is the current default. |
||
|
|
0fee50ed6e |
fix: app-router prefetch crash when an invalid URL is passed to Link (#66755)
Closes [#66650](https://github.com/vercel/next.js/issues/66650) Closes NEXT-3520 ### What? - Make Link not throw during prefetch if it received an invalid `href` (see [#66650](https://github.com/vercel/next.js/issues/66650)) - Throw in dev mode if an invalid link was passed to `router.prefetch` -- this matches current prod behavior - (previously, we'd immediately exit out of `router.prefetch`, so the user would see no indication that this'd fail in prod) ### Why? If an invalid URL was passed to `<Link>`, the whole app would crash and show "A client-side exception occurred". A failed prefetch should not bring down the whole app. Note that This preserves the current behavior of explicit `router.prefetch(url)` throwing an error if `url` is invalid. We may want to adjust this in the future, but that could be considered a breaking change, so I'm leaving it out for now. This only affects `Link`, which was intended to catch any errors thrown from `router.prefetch`, but that bit was accidentally broken. |
||
|
|
1f545aff05 |
[Breaking] disable client router cache for page segments (#66039)
This configures the default client router cache `staleTime.dynamic` value to be `0`. This means that: - Navigating between pages will always fire off a network request to get RSC data for the page segment, rather than restoring from router cache - Loading states will remain cached for 5 minutes (or whatever `config.experimental.staleTimes.static` is set to) - Shared layout data will continue to remain cached due to [partial rendering](https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#4-partial-rendering) - Back/forward behavior will still restore from cache to ensure the browser can restore scroll position. It's possible to opt-out of this, and into the previous behavior, by setting the [`staleTimes`](https://nextjs.org/docs/app/api-reference/next-config-js/staleTimes) config in `next.config.js`: ```js /** @type {import('next').NextConfig} */ const nextConfig = { experimental: { staleTimes: { dynamic: 30 }, }, } module.exports = nextConfig ``` <!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change(s) that you're making: ## For Contributors ### Improving Documentation - Run `pnpm prettier-fix` to fix formatting issues before opening the PR. - Read the Docs Contribution Guide to ensure your contribution follows the docs guidelines: https://nextjs.org/docs/community/contribution-guide ### Adding or Updating Examples - The "examples guidelines" are followed from our contributing doc https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md - Make sure the linting passes by running `pnpm build && pnpm lint`. See https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md ### Fixing a bug - Related issues linked using `fixes #number` - Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ### Adding a feature - Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas) - Related issues/discussions are linked using `fixes #number` - e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) - Documentation added - Telemetry added. In case of a feature if it's used or not. - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ## For Maintainers - Minimal description (aim for explaining to someone not on the team to understand the PR) - When linking to a Slack thread, you might want to share details of the conclusion - Link both the Linear (Fixes NEXT-xxx) and the GitHub issues - Add review comments if necessary to explain to the reviewer the logic behind a change ### What? ### Why? ### How? Closes NEXT- Fixes # --> |
||
|
|
c6320ed87a |
Replace createNextDescribe with nextTestSetup (#64817)
<!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change(s) that you're making: ## For Contributors ### Improving Documentation - Run `pnpm prettier-fix` to fix formatting issues before opening the PR. - Read the Docs Contribution Guide to ensure your contribution follows the docs guidelines: https://nextjs.org/docs/community/contribution-guide ### Adding or Updating Examples - The "examples guidelines" are followed from our contributing doc https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md - Make sure the linting passes by running `pnpm build && pnpm lint`. See https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md ### Fixing a bug - Related issues linked using `fixes #number` - Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ### Adding a feature - Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas) - Related issues/discussions are linked using `fixes #number` - e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) - Documentation added - Telemetry added. In case of a feature if it's used or not. - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ## For Maintainers - Minimal description (aim for explaining to someone not on the team to understand the PR) - When linking to a Slack thread, you might want to share details of the conclusion - Link both the Linear (Fixes NEXT-xxx) and the GitHub issues - Add review comments if necessary to explain to the reviewer the logic behind a change ### What? ### Why? ### How? Closes NEXT- Fixes # --> I took some time and [wrote a codemod](https://gist.github.com/wyattjoh/0d4464427506cb02062a4729ca906b62) that replaces the old usage of the `createNextDescribe` with the new `nextTestSetup`. You'll likely have to turn on hiding of whitespace in order to review, but this should primarily introduce no changes to the test structure other than using the new mechanism now. Closes NEXT-3178 |
||
|
|
e9aeb9e1b9 |
memoize layout router context (#64575)
Since `AppRouterState` is promise-based (so we can `use` the values and suspend in render), the state itself isn't stable between renders. Each action corresponds with a new Promise object. When a link is hovered, a prefetch action is dispatched, even if the prefetch never happens (for example, if there's already a prefetch entry in the cache, and it doesn't need to prefetch again). In other words, the prefetch action will be dispatched but won't necessarily change the state. This means that these no-op actions that don't actually change the state values will trigger a re-render. Most of the context providers in `AppRouter` are memoized with the exception of `LayoutRouter` context. This PR memoizes those values so that consumers are only notified of meaningful updates. Fixes #63159 Closes NEXT-3127 |
||
|
|
b9861fd2cd |
only prefix prefetch cache entries if they vary based on Next-URL (#61235)
### What Prefetches to pages within a shared layout would frequently cache miss despite having the data available. This causes the "instant navigation" behavior (with the 30s/5min TTL) to not be effective on these pages. ### Why In #59861, `nextUrl` was added as a prefetch cache key prefix to ensure multiple interception routes that correspond to the same URL wouldn't clash in the prefetch cache. However this causes a problem in the case where you're navigating between sub-pages. To illustrate the issue, consider the case where you load `/foo`. This will populate the prefetch cache with an entry of `{foo: <PrefetchCacheNode}`. Navigating to `/foo/bar`, with a link that prefetches back to `/foo`, will now result in a new cache node: `{foo: <PrefetchCacheNode>, /foo/bar%/foo: <PrefetchCacheNode>}` (where `Next-URL` is `/foo/bar`). Now we have a cache entry for the full data, as well as a cache entry for a partial prefetch up to the nearest loading boundary. Now when we navigate back to `/foo`, the router will see that it's missing data, and need to lazy-fetch the data triggering the loading boundary. This was especially noticeable in the case where you have a route group with it's own loading.js file because it creates a level of hierarchy in the React tree, and suspending on the data fetch would result in the group's loading boundary to be triggered. In the non-route group scenario, there's still a bug here but it would stall on the data fetch rather than triggering a boundary. ### How In #61794 we conditionally send `Next-URL` as part of the `Vary` header if we detect it could be intercepted. We use this information when creating the prefetch entry to prefix it, in case it corresponds with an intercepted route. Closes NEXT-2193 |
||
|
|
59f7ca85c2 |
remove additional static prefetch code (#59313)
This is a continuation from https://github.com/vercel/next.js/pull/58783 to remove the remaining code related to static prefetching. |
||
|
|
3043feef44 |
disable static prefetching behavior for dynamic segments (#58609)
### What? When a layout segment forces dynamic rendering (such as with `force-dynamic` or `revalidate: 0`), navigating to sub-pages of that layout will attempt to re-render the layout, also resulting in side effects re-running. This means if your layout relies on a data fetch and you render the result of that data in the layout, it will unexpectedly change when navigating between sub-paths, as described in #57326. As a separate issue (but caused by the same underlying mechanism), when using `searchParams` on a dynamic page, changes to those search params will be erroneously ignored when navigating, as described in #57075 ### Why? As a performance optimization we generate static prefetch files for dynamic segments ([original PR](https://github.com/vercel/next.js/pull/54403)). This makes it so that when prefetching is turned on, the prefetch can be served quickly from the edge without needing to invoke unnecessarily. We're able to eagerly serve things that can be safely prefetched. This is nice for cases where a path has a `loading.js` that we can eagerly render while waiting for the dynamic data to be loaded. This causes a problem with layouts that opt into dynamic rendering: when the page loads and a prefetch is kicked off for the sub-page, it'll load the static prefetch, which won't be generated with the same router state as the dynamically rendered page. This causes a mismatch between the two trees, and when navigating within the same segment, a refetch will be added to the router because it thinks that it's navigating to a new layout. This also causes issues for dynamic pages that use `searchParams`. The static prefetch will be generated without any knowledge of search params, and when the prefetch occurs, we still match to the prefetch generated without search params. This will make the router think that no change occurs, and the UI will not update to reflect the change. ### How? There's ongoing work by @acdlite to refactor the client router. Hopefully it will be easier to re-land this once that work is finished. For now, I've reverted the behavior as it doesn't seem to be worth the bugs it currently causes. I've also added tests so that when we do re-land this behavior, we can catch these subtleties. Fixes #57326 Fixes #57075 Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> |
||
|
|
07c434d54b |
Web Streams cleanup (#56819)
This updates some code related to web streams and encoding.
- Removes some unused code related to base64 encoding/decoding (Edge runtime currently supports it natively via `Buffer`)
- Prefer readable stream `pull` versus `.on("data", (chunk) => { ... })` event handlers (simplifies execution)
- Utilize `pipeTo` and `pipeThrough` on web streams to remove custom code related to stream pumping
- Updates pipe readable function to utilize web streams first class rather than relying on manual pumping + stream management
- This also takes advantage of the `AbortController` when piping so that the response can use it to cancel the stream
|
||
|
|
5a49b88fb9 |
Use consistent name for App Router tests (#56352)
Ensures all App Router tests have a unique name and similar reference (with `-`). <!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change(s) that you're making: ## For Contributors ### Improving Documentation - Run `pnpm prettier-fix` to fix formatting issues before opening the PR. - Read the Docs Contribution Guide to ensure your contribution follows the docs guidelines: https://nextjs.org/docs/community/contribution-guide ### Adding or Updating Examples - The "examples guidelines" are followed from our contributing doc https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md - Make sure the linting passes by running `pnpm build && pnpm lint`. See https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md ### Fixing a bug - Related issues linked using `fixes #number` - Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ### Adding a feature - Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas) - Related issues/discussions are linked using `fixes #number` - e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) - Documentation added - Telemetry added. In case of a feature if it's used or not. - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ## For Maintainers - Minimal description (aim for explaining to someone not on the team to understand the PR) - When linking to a Slack thread, you might want to share details of the conclusion - Link both the Linear (Fixes NEXT-xxx) and the GitHub issues - Add review comments if necessary to explain to the reviewer the logic behind a change ### What? ### Why? ### How? Closes NEXT- Fixes # --> --------- Co-authored-by: Jiachi Liu <inbox@huozhi.im> |
||
|
|
e970e050a9 |
Reland static prefetches & fix prefetch bailout behavior (#56228)
Reland #54403 Also modifies the implementation of #55950 to not change the prefetch behavior when there is flight router state - we should only check the entire loader tree in the static prefetch case, otherwise we're inadvertently rendering the component tree for prefetches that match the current flight router state segment. ([slack x-ref](https://vercel.slack.com/archives/C03S8ED1DKM/p1695862974745849)) This includes a few other misc fixes for static prefetch generation: - `next start` was not serving them (which also meant tests weren't catching a few small bugs) - the router cache key casing can differ between build and runtime, resulting in a bad cache lookup which results suspending indefinitely during navigation - We cannot generate static prefetches for pages that opt into `searchParams`, as the prefetch response won't have the right cache key in the RSC payload - Layouts that use headers/cookies shouldn't use a static prefetch because it can result in unexpected behavior (ie, being redirected to a login page, if the prefetch contains redirect logic for unauthed users) Closes NEXT-1665 Closes NEXT-1643 |
||
|
|
4c3b7f9b7a |
Export RedirectType from next/navigation (#54729)
A continuation of #51864 with RedirectType used in an e2e test and some ts-ignores removed from the codebase. Happy to split it into two PRs but I left comments in the two consequential files --------- |
||
|
|
1494283a74 |
fix: Add Next-Url to http vary in consideration of intercept routes. (#52746)
### Why We calculate the “next url” depending on the router state and the previous router state so that when you navigate to a route, the proxy matches with that header and returns you the intercepted route if matching ### What - Fixes #52745 Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com> |
||
|
|
aa3e043bbf |
Add unique search query for RSC requests to be cacable on CDN (#50970)
Adding a `_rsc` query for RSC payload requests so that they can be differentiated on resources level for CDN cache for the ones that didn't fully respect to VARY header. Also stripped them for node/edge servers so that they won't show up in the url x-ref: https://github.com/vercel/next.js/pull/49140#issuecomment-1549405009 Closes #49140 Closes NEXT-1268 |
||
|
|
25a9547cad |
Remove experimental config from create-next-app (#49241)
## What? Removes `experimental.appDir` this was leftover from when I flipped the switch. Kept the config file as in the future we might add future flags and such. It also helps that it has the types comment included so you always get types. <!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change(s) that you're making: ## For Contributors ### Improving Documentation or adding/fixing Examples - The "examples guidelines" are followed from our contributing doc https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md - Make sure the linting passes by running `pnpm build && pnpm lint`. See https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md ### Fixing a bug - Related issues linked using `fixes #number` - Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ### Adding a feature - Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas) - Related issues/discussions are linked using `fixes #number` - e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - Documentation added - Telemetry added. In case of a feature if it's used or not. - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ## For Maintainers - Minimal description (aim for explaining to someone not on the team to understand the PR) - When linking to a Slack thread, you might want to share details of the conclusion - Link both the Linear (Fixes NEXT-xxx) and the GitHub issues - Add review comments if necessary to explain to the reviewer the logic behind a change ### What? ### Why? ### How? Closes NEXT- Fixes # --> --------- Co-authored-by: JJ Kasper <jj@jjsweb.site> |
||
|
|
7de1a4070d |
Reland "app-router: new client-side cache semantics" (#48695)
Reverts vercel/next.js#48688 fix NEXT-1011 |
||
|
|
8089d0a3bb |
Revert "Reland app-router: new client-side cache semantics" (#48688)
Reverts vercel/next.js#48685 Temporary Revert again to investigate the hang job fix NEXT-1011 |