mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
codex/ledgers-react-sync
23 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f41cc4dba5 |
test: enable verified app-router deploy tests (#98524)
## Summary
Enable the same 33 previously selected deployment-test scopes across 32
app-router test files, now in a stack rooted on canary. Remove 33
`skipDeployment` options and their obsolete skip guards. 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 (33)</summary>
- ID 78:
`test/e2e/app-dir/actions-allowed-origins/app-action-allowed-origins.test.ts`
— `describe('app-dir action allowed origins', () => {
const { next } = nextTestSetup({
files: join(__dirname, 'safe-origins'),
dependencies: {
'server-only': 'latest',
},
// An arbitrary & random port.
forcedPort: 'random',
})
it('should pass if localhost is set as a safe origin', async function ()
{
const browser = await next.browser('/')
await browser.elementByCss('button').click()
await check(async () => {
return await browser.elementByCss('#res').text()
}, 'hi')
})
})`
- ID 80:
`test/e2e/app-dir/actions-allowed-origins/app-action-opaque-origin.test.ts`
— `describe('app-dir action allowed from opaque origins', () => {
const { next } = nextTestSetup({
files: join(__dirname, 'opaque-origin'),
env: {
NEXT_TEST_ALLOW_OPAQUE_ORIGIN: '1',
},
})
it('should succeed on submission', async function () {
const browser = await next.browser('/sandboxed')
await browser.elementByCss('input[type="submit"]').click()
await retry(async () => {
expect(await browser.elementByCss('output').text()).toEqual(
'Action Invoked'
)
})
})
})`
- ID 82: `test/e2e/app-dir/app-a11y/index.test.ts` — `describe('app a11y
features', () => {
const { next } = nextTestSetup({
files: __dirname,
packageJson: {},
})
describe('route announcer', () => {
async function getAnnouncerContent(browser: Playwright) {
return browser.eval(
`document.getElementsByTagName('next-route-announcer')[0]?.shadowRoot.childNodes[0]?.innerHTML`
)
}
it('should not announce the initital title', async () => {
const browser = await next.browser('/page-with-h1')
await check(() => getAnnouncerContent(browser), '')
})
it('should announce document.title changes', async () => {
const browser = await next.browser('/page-with-h1')
await browser.elementById('page-with-title').click()
await check(() => getAnnouncerContent(browser), 'page-with-title')
})
it('should announce h1 changes', async () => {
const browser = await next.browser('/page-with-h1')
await browser.elementById('noop-layout-page-1').click()
await check(() => getAnnouncerContent(browser), 'noop-layout/page-1')
})
it('should announce route changes when h1 changes inside an inner
layout', async () => {
const browser = await next.browser('/noop-layout/page-1')
await browser.elementById('noop-layout-page-2').click()
await check(() => getAnnouncerContent(browser), 'noop-layout/page-2')
})
})
})`
- ID 84: `test/e2e/app-dir/app-rendering/rendering.test.ts` —
`describe('app dir rendering', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
it('should serve app/page.server.js at /', async () => {
const html = await next.render('/')
expect(html).toContain('app/page.server.js')
})
describe('SSR only', () => {
it('should run data in layout and page', async () => {
const $ = await next.render$('/ssr-only/nested')
expect($('#layout-message').text()).toBe('hello from layout')
expect($('#page-message').text()).toBe('hello from page')
})
it('should run data fetch in parallel', async () => {
const startTime = Date.now()
const $ = await next.render$('/ssr-only/slow')
const endTime = Date.now()
const duration = endTime - startTime
// Each part takes 5 seconds so it should be below 10 seconds
// Using 7 seconds to ensure external factors causing slight slowness
don't fail the tests
expect(duration).toBeLessThan(10_000)
expect($('#slow-layout-message').text()).toBe('hello from slow layout')
expect($('#slow-page-message').text()).toBe('hello from slow page')
})
})
describe('static only', () => {
it('should run data in layout and page', async () => {
const $ = await next.render$('/static-only/nested')
expect($('#layout-message').text()).toBe('hello from layout')
expect($('#page-message').text()).toBe('hello from page')
})
it(`should run data in parallel ${
isNextDev ? 'during development' : 'and use cached version for
production'
}`, async () => {
// const startTime = Date.now()
const $ = await next.render$('/static-only/slow')
// const endTime = Date.now()
// const duration = endTime - startTime
// Each part takes 5 seconds so it should be below 10 seconds
// Using 7 seconds to ensure external factors causing slight slowness
don't fail the tests
// TODO: cache static props in prod
// expect(duration < (isDev ? 7000 : 2000)).toBe(true)
// expect(duration < 7000).toBe(true)
expect($('#slow-layout-message').text()).toBe('hello from slow layout')
expect($('#slow-page-message').text()).toBe('hello from slow page')
})
})
describe('ISR', () => {
it('should revalidate the page when revalidate is configured', async ()
=> {
const getPage = async () => {
const res = await next.fetch('isr-multiple/nested')
const html = await res.text()
return {
$: cheerio.load(html),
cacheHeader: res.headers['x-nextjs-cache'],
}
}
const { $ } = await getPage()
expect($('#layout-message').text()).toBe('hello from layout')
expect($('#page-message').text()).toBe('hello from page')
const layoutNow = $('#layout-now').text()
const pageNow = $('#page-now').text()
await waitFor(2000)
// TODO: implement
// Trigger revalidate
// const { cacheHeader: revalidateCacheHeader } = await getPage()
// expect(revalidateCacheHeader).toBe('STALE')
// TODO: implement
const { $: $revalidated /* cacheHeader: revalidatedCacheHeader */ } =
await getPage()
// expect(revalidatedCacheHeader).toBe('REVALIDATED')
const layoutNowRevalidated = $revalidated('#layout-now').text()
const pageNowRevalidated = $revalidated('#page-now').text()
// Expect that the `Date.now()` is different as the page have been
regenerated
expect(layoutNow).not.toBe(layoutNowRevalidated)
expect(pageNow).not.toBe(pageNowRevalidated)
})
})
// TODO: implement
describe.skip('mixed static and dynamic', () => {
it('should generate static data during build and use it', async () => {
const getPage = async () => {
const $ = await next.render$('isr-ssr-combined/nested')
return {
$,
}
}
const { $ } = await getPage()
expect($('#layout-message').text()).toBe('hello from layout')
expect($('#page-message').text()).toBe('hello from page')
const layoutNow = $('#layout-now').text()
const pageNow = $('#page-now').text()
const { $: $second } = await getPage()
const layoutNowSecond = $second('#layout-now').text()
const pageNowSecond = $second('#page-now').text()
// Expect that the `Date.now()` is different as it came from
getServerSideProps
expect(layoutNow).not.toBe(layoutNowSecond)
// Expect that the `Date.now()` is the same as it came from
getStaticProps
expect(pageNow).toBe(pageNowSecond)
})
})
})`
- ID 86: `test/e2e/app-dir/app-validation/validation.test.ts` —
`describe('app dir - validation', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should error when passing invalid router state tree', async () => {
const stateTree1 = JSON.stringify(['', ''])
const stateTree2 = JSON.stringify(['', {}])
const headers1 = {
rsc: '1',
'next-router-state-tree': stateTree1,
}
const headers2 = {
rsc: '1',
'next-router-state-tree': stateTree2,
}
const url1 = new URL('/', 'http://localhost')
const url2 = new URL('/', 'http://localhost')
// Add cache busting search param for both requests
const cacheBustingParam1 = await computeCacheBustingSearchParam(
undefined,
undefined,
stateTree1,
undefined
)
const cacheBustingParam2 = await computeCacheBustingSearchParam(
undefined,
undefined,
stateTree2,
undefined
)
if (cacheBustingParam1) {
url1.searchParams.set('_rsc', cacheBustingParam1)
}
if (cacheBustingParam2) {
url2.searchParams.set('_rsc', cacheBustingParam2)
}
const res = await next.fetch(url1.toString(), { headers: headers1 })
expect(res.status).toBe(500)
const res2 = await next.fetch(url2.toString(), { headers: headers2 })
expect(res2.status).toBe(200)
})
it('should generate distinct cache-busting params for known colliding
RSC variants', async () => {
const stateTree = '%5B%22%22%2C%7B%7D%5D'
const fullRequestHash = await computeCacheBustingSearchParam(
undefined,
undefined,
stateTree,
undefined
)
const prefetchRequestHash = await computeCacheBustingSearchParam(
'1',
'/_tree',
stateTree,
'/pcsta0'
)
expect(fullRequestHash).toHaveLength(16)
expect(prefetchRequestHash).toHaveLength(16)
expect(fullRequestHash).not.toBe(prefetchRequestHash)
})
it('should accept legacy cache-busting params on plain HTTP requests',
async () => {
const stateTree = '%5B%22%22%2C%7B%7D%5D'
const url = new URL('/', 'http://localhost')
const headers = {
rsc: '1',
'next-router-state-tree': stateTree,
}
url.searchParams.set(
'_rsc',
computeLegacyCacheBustingSearchParam(
undefined,
undefined,
stateTree,
undefined
)
)
const res = await next.fetch(url.toString(), {
headers,
redirect: 'manual',
})
expect(res.status).toBe(200)
})
})`
- ID 87:
`test/e2e/app-dir/async-component-preload/async-component-preload.test.ts`
— `describe('async-component-preload', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should handle redirect in an async page', async () => {
const browser = await next.browser('/')
expect(await
browser.waitForElementByCss('#success').text()).toBe('Success')
})
})`
- ID 90:
`test/e2e/app-dir/client-reference-side-effects/client-reference-side-effects.test.ts`
— `describe('client-reference-side-effects', () => {
const { next, isTurbopack } = nextTestSetup({
files: __dirname,
})
it('side effect behavior when only importing', async () => {
const browser = await next.browser('/imported')
expect(await browser.elementByCss('body').text()).toContain('Server')
let client = await browser.eval('window.client')
let client_sideeffect_reexport = await browser.eval(
'window.client_sideeffect_reexport'
)
let client_sideeffect_only = await browser.eval(
'window.client_sideeffect_only'
)
// No client references are rendered, so nothing is executed.
expect(client).toBeUndefined()
expect(client_sideeffect_reexport).toBeUndefined()
expect(client_sideeffect_only).toBeUndefined()
})
it('side effect behavior when rendering', async () => {
const browser = await next.browser('/rendered')
const body = await browser.elementByCss('body').text()
expect(body).toContain('Server')
expect(body).toContain('client component')
let client = await browser.eval('window.client')
let client_sideeffect_reexport = await browser.eval(
'window.client_sideeffect_reexport'
)
let client_sideeffect_only = await browser.eval(
'window.client_sideeffect_only'
)
expect(client).toBeTrue()
expect(client_sideeffect_reexport).toBeTrue()
if (isTurbopack) {
expect(client_sideeffect_only).toBeUndefined()
} else {
// Webpack eagerly initializes all client reference modules once at
least one of them is
// rendered.
expect(client_sideeffect_only).toBeTrue()
}
})
})`
- ID 92:
`test/e2e/app-dir/duplicate-layout-components/duplicate-layout-components.test.ts`
— `describe('app dir - duplicate layout components', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should not duplicate layout elements when navigating to 404', async
() => {
const browser = await next.browser('/solutions/404')
// Verify counts haven't changed - no duplication
expect((await browser.elementsByCss('body')).length).toBe(1)
expect((await browser.elementsByCss('#header')).length).toBe(1)
expect((await browser.elementsByCss('#footer')).length).toBe(1)
})
})`
- ID 93: `test/e2e/app-dir/dynamic-data/dynamic-data.test.ts` —
`describe('dynamic-data', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname + '/fixtures/main',
})
it('should render the dynamic apis dynamically when used in a top-level
scope', async () => {
const $ = await next.render$(
'/top-level?foo=foosearch',
{},
{
headers: {
fooheader: 'foo header value',
cookie: 'foocookie=foo cookie value',
},
}
)
if (isNextDev) {
// in dev we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else if (process.env.__NEXT_CACHE_COMPONENTS) {
// in PPR we expect the shell to be rendered at build and the page to be
rendered at runtime
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at runtime')
} else {
// in static generation we expect the entire page to be rendered at
runtime
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
}
expect($('#headers .fooheader').text()).toBe('foo header value')
expect($('#cookies .foocookie').text()).toBe('foo cookie value')
expect($('#searchparams .foo').text()).toBe('foosearch')
})
it('should render the dynamic apis dynamically when used in a top-level
scope with force dynamic', async () => {
const $ = await next.render$(
'/force-dynamic?foo=foosearch',
{},
{
headers: {
fooheader: 'foo header value',
cookie: 'foocookie=foo cookie value',
},
}
)
if (isNextDev) {
// in dev we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else if (process.env.__NEXT_CACHE_COMPONENTS) {
// @TODO this should actually be build but there is a bug in how we do
segment level dynamic in PPR at the moment
// see note in create-component-tree
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else {
// in static generation we expect the entire page to be rendered at
runtime
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
}
expect($('#headers .fooheader').text()).toBe('foo header value')
expect($('#cookies .foocookie').text()).toBe('foo cookie value')
expect($('#searchparams .foo').text()).toBe('foosearch')
})
it('should render empty objects for dynamic APIs when rendering with
force-static', async () => {
const $ = await next.render$(
'/force-static?foo=foosearch',
{},
{
headers: {
fooheader: 'foo header value',
cookie: 'foocookie=foo cookie value',
},
}
)
if (isNextDev) {
// in dev we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else if (process.env.__NEXT_CACHE_COMPONENTS) {
// in PPR we expect the shell to be rendered at build and the page to be
rendered at runtime
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
// we expect there to be a suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
} else {
// in static generation we expect the entire page to be rendered at
runtime
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
// we expect there to be no suspense boundary in fallback state
expect($('#boundary').html()).toBeNull()
}
expect($('#headers .fooheader').html()).toBeNull()
expect($('#cookies .foocookie').html()).toBeNull()
expect($('#searchparams .foo').html()).toBeNull()
})
it('should track searchParams access as dynamic when the Page is a
client component', async () => {
const $ = await next.render$(
'/client-page?foo=foosearch',
{},
{
headers: {
fooheader: 'foo header value',
cookie: 'foocookie=foo cookie value',
},
}
)
if (isNextDev) {
// in dev we expect the entire page to be rendered at runtime
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
// we don't assert the state of the fallback because it can depend on
the timing
// of when streaming starts and how fast the client references resolve
} else if (process.env.__NEXT_CACHE_COMPONENTS) {
// in PPR we expect the shell to be rendered at build and the page to be
rendered at runtime
expect($('#layout').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at runtime')
// we expect there to be a suspense boundary in fallback state
expect($('#boundary').html()).not.toBeNull()
} else {
// in static generation we expect the entire page to be rendered at
runtime
expect($('#layout').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
// we don't assert the state of the fallback because it can depend on
the timing
// of when streaming starts and how fast the client references resolve
}
expect($('#searchparams .foo').text()).toBe('foosearch')
})
if (!isNextDev) {
it('should track dynamic apis when rendering app routes', async () => {
expect(next.cliOutput).toContain(
`Caught Error: Dynamic server usage: Route /routes/url couldn't be
rendered statically because it used \`request.url\`.`
)
expect(next.cliOutput).toContain(
`Caught Error: Dynamic server usage: Route /routes/next-url couldn't be
rendered statically because it used \`nextUrl.toString\`.`
)
})
}
})`
- ID 94: `test/e2e/app-dir/dynamic-href/dynamic-href.test.ts` —
`describe('dynamic-href', () => {
const { isNextDev: isDev, next } = nextTestSetup({
files: __dirname,
})
if (isDev) {
it('should error when using dynamic href.pathname in app dir', async ()
=> {
const browser = await next.browser('/object')
await expect(browser).toDisplayRedbox(`
{
"description": "Dynamic href \`/object/[slug]\` found in <Link> while
using the \`/app\` router, this is not supported. Read more:
https://nextjs.org/docs/messages/app-dir-dynamic-href",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/object/page.js (5:5) @ HomePage
> 5 | <Link
| ^",
"stack": [
"HomePage app/object/page.js (5:5)",
],
}
`)
// Fix error
const pageContent = await next.readFile('app/object/page.js')
await next.patchFile(
'app/object/page.js',
pageContent.replace(
"pathname: '/object/[slug]'",
"pathname: '/object/slug'"
)
)
expect(await browser.waitForElementByCss('#link').text()).toBe('to
slug')
// Navigate to new page
await browser.elementByCss('#link').click()
expect(await browser.waitForElementByCss('#pathname').text()).toBe(
'/object/slug'
)
expect(await browser.elementByCss('#slug').text()).toBe('1')
})
it('should error when using dynamic href in app dir', async () => {
const browser = await next.browser('/string')
await expect(browser).toDisplayRedbox(`
{
"description": "Dynamic href \`/object/[slug]\` found in <Link> while
using the \`/app\` router, this is not supported. Read more:
https://nextjs.org/docs/messages/app-dir-dynamic-href",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/string/page.js (5:5) @ HomePage
> 5 | <Link id="link" href="/object/[slug]">
| ^",
"stack": [
"HomePage app/string/page.js (5:5)",
],
}
`)
})
} else {
it('should not error on /object in prod', async () => {
const browser = await next.browser('/object')
expect(await browser.elementByCss('#link').text()).toBe('to slug')
})
it('should not error on /string in prod', async () => {
const browser = await next.browser('/string')
expect(await browser.elementByCss('#link').text()).toBe('to slug')
})
}
})`
- ID 95:
`test/e2e/app-dir/dynamic-import-tree-shaking/dynamic-import-tree-shaking.test.ts`
— `describe('dynamic-import-tree-shaking', () => {
const { next, isNextStart, isTurbopack } = nextTestSetup({
files: __dirname,
})
// Recursively read all .js files in a directory
function getAllServerFiles(dir: string): string[] {
const results: string[] = []
try {
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
results.push(...getAllServerFiles(fullPath))
} else if (entry.name.endsWith('.js')) {
results.push(fullPath)
}
}
} catch {
// directory doesn't exist
}
return results
}
async function getAllServerContent(): Promise<string> {
const serverDir = path.join(next.testDir, '.next/server')
const files = getAllServerFiles(serverDir)
const contents = await Promise.all(
files.map((f) => fs.promises.readFile(f, 'utf8'))
)
return contents.join('\n')
}
// Verify that each page renders correctly (these should always pass in
both dev and production)
it('should render const destructure page', async () => {
const $ = await next.render$('/const-destructure')
expect($('div').text()).toContain('TREESHAKE_CONST_USED')
})
it('should render var destructure page', async () => {
const $ = await next.render$('/var-destructure')
expect($('div').text()).toContain('TREESHAKE_VAR_USED')
})
it('should render let destructure page', async () => {
const $ = await next.render$('/let-destructure')
expect($('div').text()).toContain('TREESHAKE_LET_USED')
})
it('should render rename destructure page', async () => {
const $ = await next.render$('/rename-destructure')
expect($('div').text()).toContain('TREESHAKE_RENAME_USED')
})
it('should render nested destructure page', async () => {
const $ = await next.render$('/nested-destructure')
expect($('div').text()).toContain('TREESHAKE_NESTED_USED')
})
it('should render default destructure page', async () => {
const $ = await next.render$('/default-destructure')
expect($('div').text()).toContain('TREESHAKE_DEFAULT_USED')
})
it('should render empty destructure page', async () => {
const $ = await next.render$('/empty-destructure')
expect($('div').text()).toContain('TREESHAKE_EMPTY_PAGE')
})
it('should render member access page', async () => {
const $ = await next.render$('/member-access')
expect($('div').text()).toContain('TREESHAKE_MEMBER_USED')
})
it('should render webpack-exports-comment page', async () => {
const $ = await next.render$('/webpack-exports-comment')
expect($('div').text()).toContain('TREESHAKE_COMMENT_USED')
})
it('should render rest destructure page', async () => {
const $ = await next.render$('/rest-destructure')
expect($('div').text()).toContain('TREESHAKE_REST_USED')
})
it('should render multiple imports page', async () => {
const $ = await next.render$('/multiple-imports')
expect($('div').text()).toContain('TREESHAKE_MULTI_A_USED')
expect($('div').text()).toContain('TREESHAKE_MULTI_B_USED')
})
it('should render reassign page', async () => {
const $ = await next.render$('/reassign')
expect($('div').text()).toContain('TREESHAKE_REASSIGN_USED')
})
it('should render then-arrow-destructure page', async () => {
const $ = await next.render$('/then-arrow-destructure')
expect($('div').text()).toContain('TREESHAKE_THEN_ARROW_USED')
})
it('should render then-function-destructure page', async () => {
const $ = await next.render$('/then-function-destructure')
expect($('div').text()).toContain('TREESHAKE_THEN_FUNC_USED')
})
// Tree shaking assertions: unused exports should NOT be in the server
bundle
// Tree shaking is only enabled in production builds, so skip these in
dev mode
if (isNextStart) {
it('should tree-shake unused export with const destructured dynamic
import', async () => {
const content = await getAllServerContent()
expect(content).toContain('TREESHAKE_CONST_USED')
expect(content).not.toContain('TREESHAKE_CONST_UNUSED')
})
it('should tree-shake unused export with var destructured dynamic
import', async () => {
const content = await getAllServerContent()
expect(content).toContain('TREESHAKE_VAR_USED')
expect(content).not.toContain('TREESHAKE_VAR_UNUSED')
})
it('should tree-shake unused export with let destructured dynamic
import', async () => {
const content = await getAllServerContent()
expect(content).toContain('TREESHAKE_LET_USED')
expect(content).not.toContain('TREESHAKE_LET_UNUSED')
})
it('should tree-shake unused export with renamed destructured dynamic
import', async () => {
const content = await getAllServerContent()
expect(content).toContain('TREESHAKE_RENAME_USED')
expect(content).not.toContain('TREESHAKE_RENAME_UNUSED')
})
it('should tree-shake unused export with nested destructured dynamic
import', async () => {
const content = await getAllServerContent()
expect(content).toContain('TREESHAKE_NESTED_USED')
expect(content).not.toContain('TREESHAKE_NESTED_UNUSED')
})
it('should tree-shake unused export with default destructured dynamic
import', async () => {
const content = await getAllServerContent()
expect(content).toContain('TREESHAKE_DEFAULT_USED')
expect(content).not.toContain('TREESHAKE_DEFAULT_UNUSED')
})
it('should tree-shake all exports with empty destructured dynamic
import', async () => {
const content = await getAllServerContent()
// Side effects should still be included
expect(content).toContain('TREESHAKE_EMPTY_SIDE_EFFECT')
// But no exports should be included
expect(content).not.toContain('TREESHAKE_EMPTY_USED')
expect(content).not.toContain('TREESHAKE_EMPTY_UNUSED')
})
it('should tree-shake unused export with webpackExports comment', async
() => {
const content = await getAllServerContent()
expect(content).toContain('TREESHAKE_COMMENT_USED')
expect(content).not.toContain('TREESHAKE_COMMENT_UNUSED')
})
// Member access on dynamic import is only tree-shaken by Turbopack, not
webpack
if (isTurbopack) {
it('should tree-shake unused export with member access on dynamic
import', async () => {
const content = await getAllServerContent()
expect(content).toContain('TREESHAKE_MEMBER_USED')
expect(content).not.toContain('TREESHAKE_MEMBER_UNUSED')
})
}
it('should NOT tree-shake with rest destructured dynamic import', async
() => {
const content = await getAllServerContent()
expect(content).toContain('TREESHAKE_REST_USED')
// rest elements prevent tree-shaking, so unused exports should still be
present
expect(content).toContain('TREESHAKE_REST_UNUSED')
})
it('should tree-shake unused exports with multiple dynamic imports in
one file', async () => {
const content = await getAllServerContent()
expect(content).toContain('TREESHAKE_MULTI_A_USED')
expect(content).not.toContain('TREESHAKE_MULTI_A_UNUSED')
expect(content).toContain('TREESHAKE_MULTI_B_USED')
expect(content).not.toContain('TREESHAKE_MULTI_B_UNUSED')
})
it('should NOT tree-shake with reassigned dynamic import', async () => {
const content = await getAllServerContent()
expect(content).toContain('TREESHAKE_REASSIGN_USED')
// re-assignment prevents destructuring analysis, so unused exports
should remain
expect(content).toContain('TREESHAKE_REASSIGN_UNUSED')
})
// .then() callback destructuring is only tree-shaken by Turbopack, not
webpack
if (isTurbopack) {
it('should tree-shake unused export with .then() arrow destructured
dynamic import', async () => {
const content = await getAllServerContent()
expect(content).toContain('TREESHAKE_THEN_ARROW_USED')
expect(content).not.toContain('TREESHAKE_THEN_ARROW_UNUSED')
})
it('should tree-shake unused export with .then() function destructured
dynamic import', async () => {
const content = await getAllServerContent()
expect(content).toContain('TREESHAKE_THEN_FUNC_USED')
expect(content).not.toContain('TREESHAKE_THEN_FUNC_UNUSED')
})
}
}
})`
- ID 96: `test/e2e/app-dir/dynamic-in-generate-params/index.test.ts` —
`describe('app-dir - dynamic in generate params', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should render sitemap with generateSitemaps in force-dynamic config
dynamically', async () => {
const firstTime = await getLastModifiedTime(next, 'sitemap/0.xml')
const secondTime = await getLastModifiedTime(next, 'sitemap/0.xml')
expect(firstTime).not.toEqual(secondTime)
})
it('should be able to call while generating multiple dynamic sitemaps',
async () => {
const res0 = await next.fetch('sitemap/0.xml')
const res1 = await next.fetch('sitemap/1.xml')
assertSitemapResponse(res0)
assertSitemapResponse(res1)
})
it('should be able to call fetch while generating multiple dynamic
pages', async () => {
const pageRes0 = await next.fetch('dynamic/0')
const pageRes1 = await next.fetch('dynamic/1')
expect(pageRes0.status).toBe(200)
expect(pageRes1.status).toBe(200)
})
})`
- ID 97: `test/e2e/app-dir/dynamic/dynamic.test.ts` — `describe('app dir
- next/dynamic', () => {
const { next, isNextStart, isNextDev } = nextTestSetup({
files: __dirname,
})
it('should handle ssr: false in pages when appDir is enabled', async ()
=> {
const $ = await next.render$('/legacy/no-ssr')
expect($.html()).not.toContain('navigator')
const browser = await next.browser('/legacy/no-ssr')
expect(await
browser.waitForElementByCss('#pure-client').text()).toContain(
'navigator'
)
})
it('should handle next/dynamic in SSR correctly', async () => {
const $ = await next.render$('/dynamic')
// filter out the script
const selector = 'body div'
const serverContent = $(selector).text()
// should load chunks generated via async import correctly with
React.lazy
expect(serverContent).toContain('next-dynamic lazy')
// should support `dynamic` in both server and client components
expect(serverContent).toContain('next-dynamic dynamic on server')
expect(serverContent).toContain('next-dynamic dynamic on client')
expect(serverContent).toContain('next-dynamic server import client')
expect(serverContent).not.toContain('next-dynamic dynamic no ssr on
client')
})
it('should handle next/dynamic in hydration correctly', async () => {
const browser = await next.browser('/dynamic')
await browser.waitForElementByCss('#css-text-dynamic-no-ssr-client')
expect(
await browser.elementByCss('#css-text-dynamic-no-ssr-client').text()
).toBe('next-dynamic dynamic no ssr on client:suffix')
})
it('should generate correct client manifest for dynamic chunks', async
() => {
const $ = await next.render$('/chunk-loading/server')
expect($('h1').text()).toBe('hello')
})
it('should render loading by default if loading is specified and loader
is slow', async () => {
const $ = await next.render$('/default-loading')
// First render in dev should show loading, production build will
resolve the content.
expect($('body').text()).toContain(
isNextDev ? 'Loading...' : 'This is a dynamically imported component'
)
})
it('should not render loading by default', async () => {
const $ = await next.render$('/default')
expect($('#dynamic-component').text()).not.toContain('loading')
})
it('should ignore next/dynamic in routes', async () => {
const response = await next.fetch('/api')
expect(await response.text()).toEqual('Hello function')
})
it('should ignore next/dynamic in sitemap', async () => {
const response = await next.fetch('/sitemap.xml')
expect(await
response.text()).toInclude('<changefreq>yearly</changefreq>')
})
if (isNextDev) {
it('should directly raise error when dynamic component error on server',
async () => {
const pagePath = 'app/default-loading/dynamic-component.js'
const page = await next.readFile(pagePath)
await next.patchFile(
pagePath,
page.replace('const isDevTest = false', 'const isDevTest = true')
)
await retry(async () => {
const { status } = await next.fetch('/default-loading')
expect(status).toBe(200)
})
})
}
describe('no SSR', () => {
it('should not render client component imported through ssr: false in
client components in edge runtime', async () => {
// noSSR should not show up in html
const $ = await next.render$('/dynamic-mixed-ssr-false/client-edge')
expect($('#server-false-client-module')).not.toContain(
'ssr-false-client-module-text'
)
// noSSR should not show up in browser
const browser = await
next.browser('/dynamic-mixed-ssr-false/client-edge')
expect(
await browser.elementByCss('#ssr-false-client-module').text()
).toBe('ssr-false-client-module-text')
// in the server bundle should not contain client component imported
through ssr: false
if (isNextStart) {
const middlewareManifest = JSON.parse(
await next.readFile('.next/server/middleware-manifest.json')
)
const uniquePageFiles = [
...new Set<string>(
middlewareManifest.functions[
'/dynamic-mixed-ssr-false/client-edge/page'
].files
),
]
for (const file of uniquePageFiles) {
const contents = await next.readFile(path.join('.next', file))
expect(contents).not.toContain('ssr-false-client-module-text')
}
}
})
it('should not render client component imported through ssr: false in
client components', async () => {
// noSSR should not show up in html
const $ = await next.render$('/dynamic-mixed-ssr-false/client')
expect($('#client-false-client-module')).not.toContain(
'ssr-false-client-module-text'
)
// noSSR should not show up in browser
const browser = await next.browser('/dynamic-mixed-ssr-false/client')
expect(
await browser.elementByCss('#ssr-false-client-module').text()
).toBe('ssr-false-client-module-text')
// in the server bundle should not contain both server and client
component imported through ssr: false
if (isNextStart) {
const pageServerChunk = await next.readFile(
'.next/server/app/dynamic-mixed-ssr-false/client/page.js'
)
expect(pageServerChunk).not.toContain('ssr-false-client-module-text')
}
})
it('should support dynamic import with accessing named exports from
client component', async () => {
const $ = await next.render$('/dynamic/named-export')
expect($('#client-button').text()).toBe('this is a client button')
})
it('should support dynamic import with TLA in client components', async
() => {
const $ = await next.render$('/dynamic/async-client')
expect($('#client-button').text()).toBe(
'this is an async client button with SSR'
)
expect($('#client-button-no-ssr').text()).toBe('')
const browser = await next.browser('/dynamic/async-client')
expect(await browser.elementByCss('#client-button').text()).toBe(
'this is an async client button with SSR'
)
expect(await browser.elementByCss('#client-button-no-ssr').text()).toBe(
'this is an async client button'
)
})
})
})`
- ID 101: `test/e2e/app-dir/forbidden/default/forbidden-default.test.ts`
— `describe('app dir - forbidden with default forbidden boundary', () =>
{
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
// TODO: error forbidden usage in root layout
it.skip('should error on client forbidden from root layout in browser',
async () => {
const browser = await next.browser('/')
await browser.elementByCss('#trigger-forbidden').click()
if (isNextDev) {
await waitForRedbox(browser)
expect(await getRedboxDescription(browser)).toMatch(
/forbidden\(\) is not allowed to use in root layout/
)
}
})
// TODO: error forbidden usage in root layout
it.skip('should error on server forbidden from root layout on
server-side', async () => {
const browser = await next.browser('/?root-forbidden=1')
if (isNextDev) {
await waitForRedbox(browser)
expect(await getRedboxDescription(browser)).toBe(
'Error: forbidden() is not allowed to use in root layout'
)
}
})
it('should be able to navigate to page calling forbidden', async () => {
const browser = await next.browser('/')
await browser.elementByCss('#navigate-forbidden').click()
await browser.waitForElementByCss('.next-error-h1')
expect(await browser.elementByCss('h1').text()).toBe('403')
expect(await browser.elementByCss('h2').text()).toBe(
'This page could not be accessed.'
)
})
it('should be able to navigate to page with calling forbidden in
metadata', async () => {
const browser = await next.browser('/')
await browser.elementByCss('#metadata-layout-forbidden').click()
await browser.waitForElementByCss('.next-error-h1')
expect(await browser.elementByCss('h1').text()).toBe('403')
expect(await browser.elementByCss('h2').text()).toBe(
'This page could not be accessed.'
)
})
it('should render default forbidden for group routes if forbidden is not
defined', async () => {
const browser = await next.browser('/group-dynamic/123')
expect(await browser.elementByCss('#page').text()).toBe(
'group-dynamic [id]'
)
await browser.loadPage(next.url + '/group-dynamic/403')
await waitForNoRedbox(browser)
await browser.waitForElementByCss('.group-root-layout')
expect(await browser.elementByCss('.next-error-h1').text()).toBe('403')
})
})`
- ID 102: `test/e2e/app-dir/global-error/catch-all/index.test.ts` —
`describe('app dir - global error - with catch-all route', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should render catch-all route correctly', async () => {
expect(await next.render('/en/foo')).toContain('catch-all page')
})
it('should render 404 page correctly', async () => {
expect(await next.render('/en')).toContain('This page could not be
found.')
})
it('should render global error correctly', async () => {
const browser = await next.browser('/en/error')
const text = await browser.elementByCss('#global-error').text()
expect(text).toMatchInlineSnapshot(`"global-error"`)
})
})`
- ID 103: `test/e2e/app-dir/global-error/layout-error/index.test.ts` —
`describe('app dir - global error - layout error', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
it('should render global error for error in server components', async ()
=> {
const browser = await next.browser('/')
if (isNextDev) {
await expect(browser).toDisplayRedbox(`
{
"description": "layout error",
"environmentLabel": "Server",
"label": "Runtime Error",
"source": "app/layout.js (2:9) @ layout
> 2 | throw new Error('layout error')
| ^",
"stack": [
"layout app/layout.js (2:9)",
],
}
`)
}
expect(await browser.elementByCss('h1').text()).toBe('Global Error')
expect(await browser.elementByCss('#error').text()).toBe(
isNextDev
? 'Global error: layout error'
: 'Global error: Minified React error #441; visit
https://react.dev/errors/441 for the full message or use the
non-minified dev environment for full errors and additional helpful
warnings.'
)
expect(await browser.elementByCss('#digest').text()).toMatch(/\w+/)
})
})`
- ID 114: `test/e2e/app-dir/io/io.test.ts` — `describe('io with cache
components', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname + '/fixtures/cache-components',
})
it('should make content after io() dynamic during prerender', async ()
=> {
const $ = await next.render$('/io-boundary')
if (isNextDev) {
// In dev mode everything renders at runtime
expect($('#before').text()).toBe('at runtime')
expect($('#after-io').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else {
// In production with cache components, io() creates a dynamic
// boundary. Content in the static shell is rendered at buildtime.
// Content after io() is rendered at request time because the
// hanging promise prevented it from executing during the build
prerender.
expect($('#before').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
expect($('#after-io').text()).toBe('at runtime')
}
})
it('should resolve immediately inside a "use cache" scope', async () =>
{
const $ = await next.render$('/io-in-cache')
if (isNextDev) {
expect($('#cached-value').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else {
// io() inside "use cache" is a no-op so the cached value is
// computed at cache-fill time during the build
expect($('#cached-value').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
}
})
it('should work in pages router with getServerSideProps (CC)', async ()
=> {
const $ = await next.render$('/pages-gssp')
expect($('#pages-content').text()).toBe('ok')
})
it('should work in pages router with getStaticProps (CC)', async () => {
const $ = await next.render$('/pages-gsp')
expect($('#pages-content').text()).toBe('ok')
})
it('should work in pages router with React.use() (CC)', async () => {
const $ = await next.render$('/pages-use')
expect($('#pages-content').text()).toBe('ok')
})
})`
- ID 115: `test/e2e/app-dir/io/io.test.ts` — `describe('io without cache
components', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname + '/fixtures/default',
})
it('should be a no-op during prerender without cache components', async
() => {
const $ = await next.render$('/io-boundary')
if (isNextDev) {
expect($('#before').text()).toBe('at runtime')
expect($('#after-io').text()).toBe('at runtime')
expect($('#page').text()).toBe('at runtime')
} else {
// Without cache components, io() resolves immediately during
// prerendering so the entire page is fully static
expect($('#before').text()).toBe('at buildtime')
expect($('#after-io').text()).toBe('at buildtime')
expect($('#page').text()).toBe('at buildtime')
}
})
it('should work in pages router with getServerSideProps', async () => {
const $ = await next.render$('/pages-gssp')
expect($('#pages-content').text()).toBe('ok')
})
it('should work in pages router with getStaticProps', async () => {
const $ = await next.render$('/pages-gsp')
expect($('#pages-content').text()).toBe('ok')
})
it('should work in pages router with React.use()', async () => {
const $ = await next.render$('/pages-use')
expect($('#pages-content').text()).toBe('ok')
})
})`
- ID 116: `test/e2e/app-dir/metadata-json-manifest/index.test.ts` —
`describe('app-dir metadata-json-manifest', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should support metadata.json manifest', async () => {
const response = await next.fetch('/manifest.json')
expect(response.status).toBe(200)
const json = await response.json()
expect(json).toEqual({
name: 'My Next.js Application',
short_name: 'Next.js App',
description: 'An application built with Next.js',
start_url: '/',
})
})
})`
- ID 117: `test/e2e/app-dir/metadata-suspense/index.test.ts` —
`describe('app dir - metadata dynamic routes suspense', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should render metadata in head when root layout is wrapped with
Suspense for bot requests', async () => {
const $ = await next.render$('/', undefined, {
headers: {
'User-Agent': 'Discordbot/2.0;',
},
})
expect($('head title').text()).toBe('My title')
expect($('head meta[name="application-name"]').attr('content')).toBe(
'suspense-app'
)
// unique title
expect($('title').length).toBe(1)
})
})`
- ID 119:
`test/e2e/app-dir/metadata-warnings/metadata-warnings-with-metadatabase.test.ts`
— `describe('app dir - metadata missing metadataBase', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
overrideFiles: {
'app/layout.js': `
export default function Layout({ children }) {
return (
<div>
{children}
</div>
)
}
export const metadata = {
metadataBase: new URL('https://example.com'),
}
`,
},
})
// If it's start mode, we get the whole logs since they're from build
process.
// If it's development mode, we get the logs after request
function getCliOutput(logStartPosition: number) {
return isNextDev ? next.cliOutput.slice(logStartPosition) :
next.cliOutput
}
it('should not show warning in output in default build output mode',
async () => {
const logStartPosition = next.cliOutput.length
await next.fetch('/og-image-convention')
const output = getCliOutput(logStartPosition)
expect(output).not.toInclude(METADATA_BASE_WARN_STRING)
})
it('should not warn metadataBase is missing and a relative URL is used',
async () => {
const logStartPosition = next.cliOutput.length
await next.fetch('/relative-url-og')
const output = getCliOutput(logStartPosition)
expect(output).not.toInclude(METADATA_BASE_WARN_STRING)
})
it('should warn for unsupported metadata properties', async () => {
const logStartPosition = next.cliOutput.length
await next.fetch('/unsupported-metadata')
const output = getCliOutput(logStartPosition)
expect(output).toInclude(
'Unsupported metadata themeColor is configured in metadata export in
/unsupported-metadata. Please move it to viewport'
)
expect(output).toInclude(
'Read more:
https://nextjs.org/docs/app/api-reference/functions/generate-viewport'
)
})
it('should not warn for viewport properties during manually merging
metadata', async () => {
const outputLength = next.cliOutput.length
await next.fetch('/merge')
// Should not log the unsupported metadata viewport warning in the
output
// during merging the metadata, if the value is still nullable.
const output = next.cliOutput.slice(outputLength)
expect(output).not.toContain('Unsupported metadata viewport')
})
it('should warn for deprecated fields in other property', async () => {
const logStartPosition = next.cliOutput.length
await next.fetch('/deprecated-other-fields')
const output = getCliOutput(logStartPosition)
expect(output).toInclude('Use appleWebApp instead')
expect(output).toInclude('Use icons.apple instead')
})
})`
- ID 125:
`test/e2e/app-dir/not-found-with-layout-and-group-not-found/index.test.ts`
— `describe('app dir - not found with nested layouts and custom
not-found', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should render the custom not-found page when notFound() is thrown
from a page within the group', async () => {
const browser = await next.browser('/')
await waitForNoRedbox(browser)
const heading = await browser.elementByCss('h1#not-found-heading')
expect(await heading.text()).toBe('Group Not Found Page')
})
})`
- ID 126: `test/e2e/app-dir/not-found-with-nested-layouts/index.test.ts`
— `describe('app dir - not found with nested layouts', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should render the custom not-found page when notFound() is thrown
from a page', async () => {
const browser = await next.browser('/')
await waitForNoRedbox(browser)
const heading = await browser.elementByCss('h1#not-found-heading')
expect(await heading.text()).toBe('Custom Not Found Page')
})
})`
- ID 129: `test/e2e/app-dir/not-found/default/default.test.ts` —
`describe('app dir - not-found - default', () => {
const { next, isNextStart } = nextTestSetup({
files: __dirname,
})
it('should has noindex in the head html', async () => {
const $ = await next.render$('/does-not-exist')
expect(await $('meta[name="robots"]').attr('content')).toBe('noindex')
})
if (isNextStart) {
it('should contain noindex contain in the page', async () => {
const html = await next.readFile('.next/server/app/_not-found.html')
const rsc = isPPREnabled
? 'noindex'
: await next.readFile(`.next/server/app/_not-found.rsc`)
expect(html).toContain('noindex')
expect(rsc).toContain('noindex')
})
}
})`
- ID 130:
`test/e2e/app-dir/not-found/group-route-root-not-found/index.test.ts` —
`describe('app dir - group routes with root not-found', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should render default 404 with root layout for non-existent page',
async () => {
const browser = await next.browser('/non-existent')
expect(await browser.elementByCss('p').text()).toBe('Not found
placeholder')
expect(await browser.elementByCss('h1').text()).toBe('Root layout')
})
it('should render root not found for group routes if hit 404', async ()
=> {
const browser = await next.browser('/group-dynamic/123')
expect(await browser.elementByCss('p').text()).toBe('group-dynamic
[id]')
await browser.loadPage(next.url + '/group-dynamic/404')
expect(await browser.elementByCss('p').text()).toBe('Not found
placeholder')
expect(await browser.elementByCss('h1').text()).toBe('Root layout')
})
})`
- ID 132:
`test/e2e/app-dir/parallel-routes-and-interception-nested-dynamic-routes/parallel-routes-and-interception-nested-dynamic-routes.test.ts`
— `describe('parallel-routes-and-interception-nested-dynamic-routes', ()
=> {
const { next } = nextTestSetup({
files: __dirname,
})
it('should intercept the route for nested dynamic routes', async () => {
const browser = await next.browser('/1/1')
expect(await browser.elementByCss('h1').text()).toBe('foo id 1, bar id
1')
await browser.elementByCss('a').click()
// Should intercept the route.
expect(await
browser.waitForElementByCss('p').text()).toBe('intercepted!')
// Should preserve the previous component.
expect(await browser.elementByCss('h1').text()).toBe('foo id 1, bar id
1')
await browser.refresh()
// Should display the correct /baz_id/1 content.
expect(await browser.waitForElementByCss('p').text()).toBe('baz_id/1')
})
})`
- ID 133:
`test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts`
— `describe('parallel-routes-and-interception-conflicting-pages', () =>
{
const { next } = nextTestSetup({
files: {
app: new FileRef(path.join(__dirname, 'app')),
'app/parallel/nested-2/page.js': `
export default function Page() {
return 'hello world'
}
`,
},
nextConfig,
})
it('should gracefully handle when two page segments match the `children`
parallel slot', async () => {
const html = await next.render('/parallel/nested-2')
// before adding this file, the page would have matched
`/app/parallel/(new)/@baz/nested-2/page`
// but we've added a more specific page, so it should match that instead
if (process.env.IS_TURBOPACK_TEST) {
// TODO: this matches differently in Turbopack because the Webpack
loader does some sorting on the paths
// Investigate the discrepancy in a follow-up. For now, since no errors
are being thrown (and since this test was previously ignored in
Turbopack),
// we'll just verify that the page is rendered and some content was
matched.
expect(html).toContain('parallel/(new)/@baz/nested/page')
} else {
expect(html).toContain('hello world')
}
})
})`
- ID 134:
`test/e2e/app-dir/parallel-routes-not-found/parallel-routes-not-found.test.ts`
— `describe('parallel-routes-and-interception', () => {
const { next } = nextTestSetup({
files: __dirname,
})
// TODO: revisit the error for missing parallel routes slot
it('should not render the @children slot when the @slot is not found',
async () => {
const browser = await next.browser('/')
// we make sure the page is available through navigating
expect(await browser.elementByCss('body').text()).toMatch(
/This page could not be found/
)
// we also check that the #children-slot id is not present
expect(await
browser.hasElementByCssSelector('#children-slot')).toBe(false)
await retry(async () => {
const title = await browser.eval(() => {
return document.title
})
// TODO: the fact that the title on the client (in hydration data)
disagrees with the title SSRd
// when cache components is off is a sign we don't have coherent
handling of notFound titles
// This test now asserts the prod client title in next start that would
actually be observed
// by site visitors post hydration.
expect(title).toBe('404: This page could not be found.')
})
})
it('should render the title once for the non-existed route', async () =>
{
const browser = await next.browser('/non-existed')
const titles = await browser.elementsByCss('title')
// FIXME: (metadata), the title should only be rendered once and using
the not-found title
expect(titles).toHaveLength(3)
})
})`
- ID 137: `test/e2e/app-dir/root-layout-render-once/index.test.ts` —
`describe('app-dir root layout render once', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should only render root layout once', async () => {
let $ = await next.render$('/render-once')
expect($('#counter').text()).toBe('0')
$ = await next.render$('/render-once')
expect($('#counter').text()).toBe('1')
$ = await next.render$('/render-once')
expect($('#counter').text()).toBe('2')
})
})`
- ID 138: `test/e2e/app-dir/root-layout/root-layout.test.ts` —
`describe('app-dir root layout', () => {
const { next, isNextDev: isDev } = nextTestSetup({
files: __dirname,
})
if (isDev) {
// TODO-APP: re-enable after reworking the error overlay.
describe.skip('Missing required tags', () => {
it('should error on page load', async () => {
const browser = await next.browser('/missing-tags', {
waitHydration: false,
})
await waitForRedbox(browser)
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
"Please make sure to include the following tags in your root layout:
<html>, <body>.
Missing required root layout tags: html, body"
`)
})
it('should error on page navigation', async () => {
const browser = await next.browser('/has-tags', {
waitHydration: false,
})
await browser.elementByCss('a').click()
await waitForRedbox(browser)
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
"Please make sure to include the following tags in your root layout:
<html>, <body>.
Missing required root layout tags: html, body"
`)
})
it('should error on page load on static generation', async () => {
const browser = await next.browser('/static-missing-tags/slug', {
waitHydration: false,
})
await waitForRedbox(browser)
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
"Please make sure to include the following tags in your root layout:
<html>, <body>.
Missing required root layout tags: html, body"
`)
})
})
}
describe('Should do a mpa navigation when switching root layout', () =>
{
it('should work with basic routes', async () => {
const browser = await next.browser('/basic-route')
expect(await browser.elementById('basic-route').text()).toBe(
'Basic route'
)
await browser.eval('window.__TEST_NO_RELOAD = true')
// Navigate to page with same root layout
await browser.elementByCss('a').click()
expect(
await browser.waitForElementByCss('#inner-basic-route').text()
).toBe('Inner basic route')
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue()
// Navigate to page with different root layout
await browser.elementByCss('a').click()
expect(await browser.waitForElementByCss('#route-group').text()).toBe(
'Route group'
)
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined()
})
it('should work with route groups', async () => {
const browser = await next.browser('/route-group')
expect(await browser.elementById('route-group').text()).toBe(
'Route group'
)
await browser.eval('window.__TEST_NO_RELOAD = true')
// Navigate to page with same root layout
await browser.elementByCss('a').click()
expect(
await browser.waitForElementByCss('#nested-route-group').text()
).toBe('Nested route group')
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue()
// Navigate to page with different root layout
await browser.elementByCss('a').click()
expect(await browser.waitForElementByCss('#parallel-one').text()).toBe(
'One'
)
expect(await browser.waitForElementByCss('#parallel-two').text()).toBe(
'Two'
)
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined()
})
it('should work with parallel routes', async () => {
const browser = await next.browser('/with-parallel-routes')
expect(await browser.elementById('parallel-one').text()).toBe('One')
expect(await browser.elementById('parallel-two').text()).toBe('Two')
await browser.eval('window.__TEST_NO_RELOAD = true')
// Navigate to page with same root layout
await check(async () => {
await browser.elementByCss('a').click()
expect(
await browser.waitForElementByCss('#parallel-one-inner').text()
).toBe('One inner')
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue()
return 'success'
}, 'success')
// Navigate to page with different root layout
await check(async () => {
await browser.elementByCss('a').click()
expect(await browser.waitForElementByCss('#dynamic-hello').text()).toBe(
'dynamic hello'
)
return 'success'
}, 'success')
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined()
})
it('should work with dynamic routes', async () => {
const browser = await next.browser('/dynamic/first')
expect(await browser.elementById('dynamic-first').text()).toBe(
'dynamic first'
)
await browser.eval('window.__TEST_NO_RELOAD = true')
// Navigate to page with same root layout
await browser.elementByCss('a').click()
expect(
await browser.waitForElementByCss('#dynamic-first-second').text()
).toBe('dynamic first second')
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue()
// Navigate to page with different root layout
await browser.elementByCss('a').click()
expect(
await browser.waitForElementByCss('#inner-basic-route').text()
).toBe('Inner basic route')
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined()
})
it('should work with dynamic catchall routes', async () => {
const browser = await next.browser('/dynamic-catchall/slug')
expect(await browser.elementById('catchall-slug').text()).toBe(
'catchall slug'
)
await browser.eval('window.__TEST_NO_RELOAD = true')
// Navigate to page with same root layout
await browser.elementById('to-next-url').click()
expect(
await browser.waitForElementByCss('#catchall-slug-slug').text()
).toBe('catchall slug slug')
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue()
// Navigate to page with different root layout
await browser.elementById('to-dynamic-first').click()
expect(await browser.elementById('dynamic-first').text()).toBe(
'dynamic first'
)
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined()
})
it('should work with static routes', async () => {
const browser = await next.browser('/static-mpa-navigation/slug1')
expect(await browser.elementById('static-slug1').text()).toBe(
'static slug1'
)
await browser.eval('window.__TEST_NO_RELOAD = true')
// Navigate to page with same root layout
await browser.elementByCss('a').click()
expect(await browser.waitForElementByCss('#static-slug2').text()).toBe(
'static slug2'
)
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeTrue()
// Navigate to page with different root layout
await browser.elementByCss('a').click()
expect(await browser.elementById('basic-route').text()).toBe(
'Basic route'
)
expect(await browser.eval('window.__TEST_NO_RELOAD')).toBeUndefined()
const res = await next.fetch(
`${next.url}/static-mpa-navigation/slug-not-existed`
)
expect(res.status).toBe(404)
})
})
it('should correctly handle navigation between multiple root layouts',
async () => {
const browser = await next.browser('/root-layout-a')
await browser.waitForElementByCss('#root-a')
expect(await browser.hasElementByCssSelector('#root-b')).toBeFalse()
await browser
.elementById('link-to-b')
.click()
.waitForElementByCss('#root-b')
expect(await browser.hasElementByCssSelector('#root-a')).toBeFalse()
})
it('should correctly handle navigation between multiple root layouts
when redirecting in a server action', async () => {
const browser = await next.browser('/root-layout-a')
await browser.waitForElementByCss('#action-redirect-to-b')
expect(await browser.hasElementByCssSelector('#root-b')).toBeFalse()
await browser
.elementById('action-redirect-to-b')
.click()
.waitForElementByCss('#root-b')
expect(await browser.hasElementByCssSelector('#root-a')).toBeFalse()
})
})`
- ID 139:
`test/e2e/app-dir/root-suspense-dynamic/root-suspense-dynamic.test.ts` —
`describe('Root Suspense Dynamic Rendering', () => {
const { next, isNextStart } = nextTestSetup({
files: __dirname + '/fixtures/default',
})
// TODO: remove when there is a test for isNextDev === false
it('placeholder to satisfy at least one test when isNextDev is false',
async () => {
expect(true).toBe(true)
})
if (isNextStart) {
it('should handle dynamic content wrapped in Suspense above HTML
structure', async () => {
try {
// Should render the page successfully
const $ = await next.render$('/')
expect($('body').text()).toContain('Hello World')
} catch (error) {
throw new Error(
'Expected build to succeed for Suspense wrapping dynamic content above
HTML',
{ cause: error }
)
}
})
it('should correctly mark route as dynamic', async () => {
// The route should be marked as dynamic (ƒ) not static (○)
expect(next.cliOutput).toContain('ƒ /')
})
}
})`
- ID 140:
`test/e2e/app-dir/similar-pages-paths/similar-pages-paths.test.ts` —
`describe('app-dir similar pages paths', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should not have conflicts for similar pattern page paths between app
and pages', async () => {
// pages/page and app/page
const res1 = await next.fetch('/')
expect(res1.status).toBe(200)
expect(await res1.text()).toContain('(app/page.js)')
const res2 = await next.fetch('/page')
expect(res2.status).toBe(200)
expect(await res2.text()).toContain('(pages/page.js)')
})
})`
- ID 143:
`test/e2e/app-dir/unauthorized/default/unauthorized-default.test.ts` —
`describe('app dir - unauthorized with default unauthorized boundary',
() => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
// TODO: error unauthorized usage in root layout
it.skip('should error on client unauthorized from root layout in
browser', async () => {
const browser = await next.browser('/')
await browser.elementByCss('#trigger-unauthorized').click()
if (isNextDev) {
await waitForRedbox(browser)
expect(await getRedboxDescription(browser)).toMatch(
/unauthorized\(\) is not allowed to use in root layout/
)
}
})
// TODO: error unauthorized usage in root layout
it.skip('should error on server unauthorized from root layout on
server-side', async () => {
const browser = await next.browser('/?root-unauthorized=1')
if (isNextDev) {
await waitForRedbox(browser)
expect(await getRedboxDescription(browser)).toBe(
'Error: unauthorized() is not allowed to use in root layout'
)
}
})
it('should be able to navigate to page calling unauthorized', async ()
=> {
const browser = await next.browser('/')
await browser.elementByCss('#navigate-unauthorized').click()
await browser.waitForElementByCss('.next-error-h1')
expect(await browser.elementByCss('h1').text()).toBe('401')
expect(await browser.elementByCss('h2').text()).toBe(
`You're not authorized to access this page.`
)
})
it('should be able to navigate to page with calling unauthorized in
metadata', async () => {
const browser = await next.browser('/')
await browser.elementByCss('#metadata-layout-unauthorized').click()
await browser.waitForElementByCss('.next-error-h1')
expect(await browser.elementByCss('h1').text()).toBe('401')
expect(await browser.elementByCss('h2').text()).toBe(
`You're not authorized to access this page.`
)
})
it('should render default unauthorized for group routes if unauthorized
is not defined', async () => {
const browser = await next.browser('/group-dynamic/123')
expect(await browser.elementByCss('#page').text()).toBe(
'group-dynamic [id]'
)
await browser.loadPage(next.url + '/group-dynamic/401')
await waitForNoRedbox(browser)
await browser.waitForElementByCss('.group-root-layout')
expect(await browser.elementByCss('.next-error-h1').text()).toBe('401')
})
})`
</details>
<details>
<summary>Deployment evidence for the additional scopes</summary>
- `test/e2e/app-dir/app-rendering/rendering.test.ts` — `describe('app
dir rendering', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677110);
Cache Components excluded by manifest.
- `test/e2e/app-dir/dynamic-data/dynamic-data.test.ts` —
`describe('dynamic-data', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677056);
Cache Components excluded by manifest.
- `test/e2e/app-dir/forbidden/default/forbidden-default.test.ts` —
`describe('app dir - forbidden with default forbidden boundary', () =>
{`:
[normal](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677074);
Cache Components excluded by manifest.
- `test/e2e/app-dir/root-layout/root-layout.test.ts` —
`describe('app-dir root layout', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677110);
Cache Components excluded by manifest.
- `test/e2e/app-dir/unauthorized/default/unauthorized-default.test.ts` —
`describe('app dir - unauthorized with default unauthorized boundary',
() => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677110);
Cache Components excluded by manifest.
-
`test/e2e/app-dir/actions-allowed-origins/app-action-opaque-origin.test.ts`
— `describe('app-dir action allowed from opaque origins', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677074),
[cache](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677108);
this scope passed although another scope in the file failed.
-
`test/e2e/app-dir/parallel-routes-and-interception/parallel-routes-and-interception.test.ts`
— `describe('parallel-routes-and-interception-conflicting-pages', () =>
{`:
[normal](https://github.com/vercel/next.js/actions/runs/34426784855/job/102715677110);
Cache Components excluded by manifest; this scope passed although
another scope in the file failed.
</details>
<!-- NEXT_JS_LLM -->
|
||
|
|
e9180eae2a |
errors: shorten "use cache" messages and unify them into one factory (#94300)
### What? Centralizes related <code>"use cache"</code> scope errors and rewrites them as shorter, actionable messages with consistent <code>Learn more:</code> links. It also updates the reachable revalidation errors for rendering and <code>generateStaticParams</code>. ### Why? The previous messages were long, inconsistent, and sometimes missing documentation links. Some new cache-specific revalidation messages were also hidden by an earlier render-phase error. The new messages name the constraint, give the immediate fix, and link to the relevant documentation. ### How? Adds centralized error factories and dedicated error pages for request data, cache configuration, private-cache composition, and revalidation. When an active App Router route is available, the messages include it. The error pages use consistent terminology and complete examples. The <code>unstable_cache()</code> reference now documents the request-dependent operations that these errors reject. ### Before and after Route-based examples use <code>/products</code>. Bracketed values are alternatives in matching order. For example, <code>[E1482, E1486, E1489]</code> maps to <code>[headers(), cookies(), request.url]</code>. Each runtime error contains one alternative, but the table groups messages with the same template. #### Messages that include the route | Cases | Before | After | | --- | --- | --- | | E1480 · <code>searchParams</code> in <code>"use cache"</code> | Route /products used <code>searchParams</code> inside "use cache". Accessing dynamic request data inside a cache scope is not supported. If you need some search params inside a cached function await <code>searchParams</code> outside of the cached function and pass only the required search params as arguments to the cached function. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache | Route "/products": <code>searchParams</code> can't be read inside <code>"use cache"</code>. Await it outside the cached function and pass what you need as an argument.<br><br>Learn more: https://nextjs.org/docs/messages/next-request-in-use-cache | | [E1482, E1486, E1489] · [<code>headers()</code>, <code>cookies()</code>, <code>request.url</code>] in <code>"use cache"</code> | Route /products used [<code>headers()</code>, <code>cookies()</code>, <code>request.url</code>] inside "use cache". Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use [<code>headers()</code>, <code>cookies()</code>, <code>request.url</code>] outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache | Route "/products": [<code>headers()</code>, <code>cookies()</code>, <code>request.url</code>] can't be read inside <code>"use cache"</code>. Read it outside the cached function and pass what you need as an argument.<br><br>Learn more: https://nextjs.org/docs/messages/next-request-in-use-cache | | [E1481, E1485, E1492] · [<code>cookies()</code>, <code>request.url</code>, <code>headers()</code>] in <code>unstable_cache()</code> | Route /products used [<code>cookies()</code>, <code>request.url</code>, <code>headers()</code>] inside a function cached with <code>unstable_cache()</code>. Accessing Dynamic data sources inside a cache scope is not supported. If you need this data inside a cached function use [<code>cookies()</code>, <code>request.url</code>, <code>headers()</code>] outside of the cached function and pass the required dynamic data in as an argument. See more info here: https://nextjs.org/docs/app/api-reference/functions/unstable_cache | Route "/products": [<code>cookies()</code>, <code>request.url</code>, <code>headers()</code>] can't be read inside <code>unstable_cache()</code>. Read it outside the cached function and pass what you need as an argument.<br><br>Learn more: https://nextjs.org/docs/app/api-reference/functions/unstable_cache | | [E1484, E1491] · <code>draftMode().enable()</code> in [<code>unstable_cache()</code>, <code>"use cache"</code>] | Route /products used "draftMode().enable()" inside [a function cached with <code>unstable_cache()</code>, "use cache"]. The enabled status of <code>draftMode()</code> can be read in caches but you must not enable or disable <code>draftMode()</code> inside a cache. See more info here: [https://nextjs.org/docs/app/api-reference/functions/unstable_cache, https://nextjs.org/docs/messages/next-request-in-use-cache] | Route "/products": <code>draftMode().enable()</code> can't be called inside [<code>unstable_cache()</code>, <code>"use cache"</code>]. Draft mode can be read inside a cached function, but enabling or disabling it must happen outside.<br><br>Learn more: [https://nextjs.org/docs/app/api-reference/functions/unstable_cache, https://nextjs.org/docs/messages/next-request-in-use-cache] | | [E1488, E1499] · <code>connection()</code> in [<code>unstable_cache()</code>, <code>"use cache"</code>] | Route /products used <code>connection()</code> inside [a function cached with <code>unstable_cache()</code>, "use cache"]. The <code>connection()</code> function is used to indicate the subsequent code must only run when there is an actual request, but caches must be able to be produced before a request, so this function is not allowed in this scope. See more info here: [https://nextjs.org/docs/app/api-reference/functions/unstable_cache, https://nextjs.org/docs/messages/next-request-in-use-cache] | Route "/products": <code>connection()</code> can't be called inside [<code>unstable_cache()</code>, <code>"use cache"</code>] because cached functions may run during prerendering, without an incoming request. Call it outside the cached function.<br><br>Learn more: [https://nextjs.org/docs/app/api-reference/functions/unstable_cache, https://nextjs.org/docs/messages/next-request-in-use-cache] | | E1494 · <code>connection()</code> in <code>"use cache: private"</code> | Route /products used <code>connection()</code> inside "use cache: private". The <code>connection()</code> function is used to indicate the subsequent code must only run when there is an actual navigation request, but caches must be able to be produced before a navigation request, so this function is not allowed in this scope. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache | Route "/products": <code>connection()</code> can't be called inside <code>"use cache: private"</code> because private cached functions may run during prefetching, without a navigation request. Call it outside the cached function.<br><br>Learn more: https://nextjs.org/docs/app/api-reference/directives/use-cache-private | | [E1483, E1495] · <code>revalidateTag("products")</code> in [<code>"use cache"</code>, <code>unstable_cache()</code>] | Route /products used "revalidateTag products" inside [a "use cache", a function cached with "unstable_cache(...)"] which is unsupported. To ensure revalidation is performed consistently it must always happen outside of renders and cached functions. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering | Route "/products": <code>revalidateTag("products")</code> can't be called during render, inside a cached function, or inside <code>generateStaticParams</code>. Call it from a Server Action or Route Handler instead.<br><br>Learn more: https://nextjs.org/docs/messages/revalidate-in-use-cache | | During render | Route /products used "revalidateTag products" during render which is unsupported. To ensure revalidation is performed consistently it must always happen outside of renders and cached functions. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering | Route "/products": <code>revalidateTag("products")</code> can't be called during render, inside a cached function, or inside <code>generateStaticParams</code>. Call it from a Server Action or Route Handler instead.<br><br>Learn more: https://nextjs.org/docs/messages/revalidate-in-use-cache | | Inside <code>generateStaticParams</code> | Route /products used "revalidateTag products" inside <code>generateStaticParams</code> which is unsupported. To ensure revalidation is performed consistently it must always happen outside of renders and cached functions. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering | Route "/products": <code>revalidateTag("products")</code> can't be called inside <code>generateStaticParams</code>. Call it from a Server Action or Route Handler instead.<br><br>Learn more: https://nextjs.org/docs/messages/revalidate-in-use-cache | #### Other messages | Cases | Before | After | | --- | --- | --- | | [E1477, E1490] · Nested cache with [short <code>expire</code>, <code>revalidate: 0</code>] | A "use cache" with [short <code>expire</code> (under 5 minutes), zero <code>revalidate</code>] is nested inside another "use cache" that has no explicit <code>cacheLife</code>, which is not allowed during prerendering. Add <code>cacheLife()</code> to the outer "use cache" to choose whether it should be prerendered [with longer <code>expire</code>, with non-zero <code>revalidate</code>] or remain dynamic [with short <code>expire</code>, with zero <code>revalidate</code>]. Read more: https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife | Route <code>"/products"</code>: A nested <code>"use cache"</code> with [a short <code>expire</code> (under 5 minutes), <code>revalidate: 0</code>] is inside an outer <code>"use cache"</code> that has no <code>cacheLife()</code>. Add <code>cacheLife()</code> to the outer one to choose whether to prerender it [with a longer <code>expire</code>, with a non-zero <code>revalidate</code>] or keep it dynamic [with a short <code>expire</code>, with <code>revalidate: 0</code>].<br><br>Learn more: https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife | | E1478 · External promise | Filling a "use cache" entry appears to be stuck on shared state from the outer render scope. The same function completed when run in isolation, which usually means a module-scoped value (for example a top-level Map used to dedupe fetches) is joining a promise created outside the cache. "use cache" already dedupes calls with the same arguments within a request and across requests on the same server instance, so the surrounding dedupe layer is both unnecessary and the likely cause. Remove it and rely on "use cache" alone for deduping.<br><br><em>No documentation link.</em> | Route <code>"/products"</code>: A <code>"use cache"</code> function is awaiting a promise created outside it. The same call completed when run in isolation, so a module-scoped value (often a top-level <code>Map</code> used to dedupe fetches) is most likely blocking it. <code>"use cache"</code> already dedupes calls with the same arguments. Remove the surrounding dedupe layer.<br><br>Learn more: https://nextjs.org/docs/messages/next-request-in-use-cache | | [E1479, E1498] · [<code>cacheTag()</code>, <code>cacheLife()</code>] outside a cached function | [<code>cacheTag()</code>, <code>cacheLife()</code>] can only be called inside a "use cache" function.<br><br><em>No documentation link.</em> | Route <code>"/products"</code>: [<code>cacheTag()</code>, <code>cacheLife()</code>] can only be called inside a <code>"use cache"</code> or <code>"use cache: private"</code> function.<br><br>Learn more: [https://nextjs.org/docs/messages/cache-tag-outside-use-cache, https://nextjs.org/docs/messages/cache-life-outside-use-cache]<br><br>Outside an App Router route, the same message is shown without the route prefix. | | E1487 · Prerender timeout | Filling a cache during prerender timed out, likely because request-specific arguments such as params, searchParams, cookies() or dynamic data were used inside "use cache".<br><br><em>No documentation link.</em> | Route <code>"/products"</code>: A <code>"use cache"</code> function took too long during prerendering. The most common cause is passing unresolved request-specific arguments, such as <code>params</code> or <code>searchParams</code>, into the cached function. Resolve the data before calling the function and pass only the values you need.<br><br>Learn more: https://nextjs.org/docs/messages/next-request-in-use-cache | | E1493 · Private cache inside a public cache | "use cache: private" must not be used within "use cache". It can only be nested inside of another "use cache: private".<br><br><em>No documentation link.</em> | Route <code>"/products"</code>: <code>"use cache: private"</code> can't be nested inside <code>"use cache"</code> because a shared cached function can't depend on private request data. Nest it only inside another <code>"use cache: private"</code>.<br><br>Learn more: https://nextjs.org/docs/messages/use-cache-private-composition | | E1496 · Private cache without a request | "use cache: private" cannot be used outside of a request context.<br><br><em>No documentation link.</em> | Route <code>"/products"</code>: <code>"use cache: private"</code> needs an active request, so it can't be used during <code>generateStaticParams</code> or other build-time contexts. Move it to a request-time component or function.<br><br>Learn more: https://nextjs.org/docs/messages/use-cache-private-composition | | E1497 · Private cache inside <code>unstable_cache()</code> | "use cache: private" must not be used within <code>unstable_cache()</code>.<br><br><em>No documentation link.</em> | Route <code>"/products"</code>: <code>"use cache: private"</code> can't be used inside <code>unstable_cache()</code> because <code>unstable_cache()</code> uses a shared cache that can't contain private request data. Call the private cached function outside <code>unstable_cache()</code>.<br><br>Learn more: https://nextjs.org/docs/messages/use-cache-private-composition | ### Runtime verification - Exercised the 13 route-based message IDs through minimal dev-runtime reproductions. Twelve reached the new factories from userland, with byte-identical output across three requests each. - The `request.url` error for `"use cache"` Route Handlers is currently limited to the prerender path. A dynamic request captured by a cached closure can bypass that tracking; this is existing framework behavior to follow up separately. - Passing the `searchParams` promise into a nested cached function can still surface the earlier synchronous dynamic-API error before this factory. The new message is verified when the cache scope reads its own `searchParams` value. - A follow-up preview verification triggered 17 of the 19 rewritten messages from userland with byte-identical output. The timeout-driven external-promise and prerender-timeout messages were verified in source and through their focused test coverage. ### Verification - <code>CI=1 pnpm build-all</code> - <code>pnpm --filter=next types</code> - <code>pnpm --filter=next build</code> - <code>HEADLESS=true pnpm test-dev-turbo test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts</code> - <code>HEADLESS=true pnpm test-start-turbo test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts</code> - <code>NEXT_SKIP_ISOLATE=1 HEADLESS=true pnpm test-dev-webpack test/e2e/app-dir/cache-components-errors/use-cache.test.ts -t 'cacheLife with (expire < 5 minutes|revalidate: 0)'</code> (6 tests and 6 snapshots passed) - <code>pnpm test-dev test/e2e/app-dir/cache-components-errors/use-cache.test.ts --projects jest.config.*</code> (42 tests and 42 snapshots passed across Turbopack and webpack) - <code>pnpm test-dev test/e2e/app-dir/use-cache-hanging/use-cache-hanging.test.ts --projects jest.config.*</code> (10 tests and 8 snapshots passed across Turbopack and webpack) - <code>pnpm test-dev test/e2e/app-dir/use-cache-configured-timeout/use-cache-configured-timeout.test.ts --projects jest.config.*</code> (4 tests and 2 snapshots passed across Turbopack and webpack) - Prettier, ESLint, and Alex on the changed source, tests, and error pages <!-- NEXT_JS_LLM --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d7aa66c345 |
Remove generated error codes (#97687)
### Why? Should come up with better solution that does not block PRs with git conflict x-ref: https://vercel.slack.com/archives/C02CDC2ALJH/p1785263902728189?thread_ts=1785263687.502649&cid=C02CDC2ALJH ### How? - Delete `errors.json`, the error-code SWC plugin, generated WASM, merge driver, and validation/build tooling. - Stop attaching error codes to server-rendering digests, redboxes, and telemetry; native `Error.code` and `Error.name` remain available where applicable. - Remove the development-overlay error feedback UI, middleware, and telemetry event that depended on stable codes. - Update fixtures, snapshots, and guidance for code-free errors and numeric-only digests. <!-- NEXT_JS_LLM --> |
||
|
|
7d98e0b534 | [test] Include error code in Redbox snapshot (#90497) | ||
|
|
85c3d20a91 |
[cache components]: move flag out of experimental (#85035)
This moves `experimental.cacheComponents` to a top level config. As part of this, I disabled some tests in `build-output-prerender` that assert on `cacheComponents` appearing in the experimental list. In a separate PR, I'm going to show that Cache Components is enabled next to the bundler info. This also updates some docs pages to remove "experimental" language. |
||
|
|
d81ef2691c |
feat(breaking): Hard Deprecate PPR Configuration (#84280)
This hard deprecates the `experimental.ppr` configuration, requiring users to opt-in instead via `experimental.cacheComponents`. This does mean that the previous `experimental.ppr = "incremental"` will no longer be supported. NAR-433 |
||
|
|
1dda5b668b |
[Breaking] Remove deprecated sync access to Dynamic APIs (#84179)
This PR removes the deprecated sync access to Dynamic APIs. - Removed UnsafeUnwrapped* types. - Replaced `as unknown as UnsafeUnwrapped ` type casts to `as any` for dev warning. - Removed tests that expected sync access to not error. - Removed `UntrackedExotic` functions. - Modified tests that were accidentally doing sync access. - Updated warnings to emphasize that it is a Promise and must be awaited. - Removed paragraph that states access is allowed from `errors/sync-dynamic-apis.mdx`. --------- Co-authored-by: Josh Story <story@hey.com> |
||
|
|
80ad3b8dc4 |
Upgrade React from a7a11657-20250708 to 60b5271a-20250709 (#81463)
Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com> |
||
|
|
9bbcfa2c14 |
Upgrade React from 5d87cd22-20250704 to a7a11657-20250708 (#81428)
Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com> |
||
|
|
16e1e1455e |
[node-webstreams] Use React builds for Node.js (#81048)
Closes https://linear.app/vercel/issue/NEXT-4560/ React now exposes Web stream APIs in their Node.js builds so we can use the Node.js builds. That enable us to use some of the Node.js goodies like `async_hooks` which is required for the React's experimental Server Requests track. ## Test plan `next-server` runtimes did not increase bundle size. We even saved some in the `app-page` entries since we no longer have to bundle Edge and Node.js versions of React Server. We can rely solely on the Node.js variant of React Server. A build of `test/e2e/app-dir/hello-world` shows no significant bundle size changes |
||
|
|
8ea7d4bb35 |
[dev-overlay] Move error.name to label (#78198)
|
||
|
|
19e2edcf4a |
Reland "[dynamicIO] warn for disallowed dynamic in dev" (#71567)
Reverts vercel/next.js#71566 which relands #71526 merge after 15 is released |
||
|
|
c85914c1f6 |
Revert "[dynamicIO] warn for disallowed dynamic in dev" (#71566)
We're going to land this after 15. Reverts vercel/next.js#71526 |
||
|
|
b18c027ac6 |
[dynamicIO] warn for disallowed dynamic in dev (#71526)
Previously our rules for allowed dynamic were only enforced in builds. This is not tenable for serious adoption because you can't spend all day working on your site and then find out its broken when you build / deploy. To address this I've added a simulated prerendering during dev that should enforce the same rules for rendered routes in dev that would apply during a build. There is no way to avoid doing additional work to implement this feature because we need the abort mechanism of SSR to actually find the dynamic holes that should be disallowed. I was able to avoid completely duplicating the RSC render however by capturing the stream chunks available before the first task completes and stashing them away as a simulated halted RSC payload. I did have to introduce prospective renders to the RSC render and the validating SSR render however to work around issues of lazy module initialization where functions like Math.random and new Date() in module scope would incorrectly trigger dynamic validation when you would not expect them to. |
||
|
|
e2ed2c567a |
[dynamicIO] Error when dynamic behavior is outside of a Suspense boundary (#70734)
When `dynamicIO` is enabled it is important that you can reason about what parts of component tree are allowed to be dynamic and which must be static. The heuristic we're going with is to require that anything dynamic must be rendered inside a Suspense boundary. The rationale here is that if you have a static component then you don't need to define a fallback UI for it because it will never render. Conversely to have a useful dynamic site having a well defined fallback UI to show when the dynamic part is not ready yet is helpful. If PPR is turned off the Suspense boundaries won't actually define a static UI during prerendering but we want the upgrade path to PPR to be as unblocked as possible so we require that you follow this semantic when `dynamicIO` is enabled even if PPR is not. There are some special considerations we need to make for `generateMetadata`, `generateViewport`, and use of synchronosuly dynamic APIs like `(cookies() as unknown as UnsafeUnwrappedCookies).get(...)`. Metadata & Viewport can't be wrapped in a Suspense boundary. So if something else in your application is dynamic we don't error for dynamic metadata/viewport. If dynamic metadata/viewport is the only thing that is dynamic in your application we consider this an error. Your recourse is to make these functions only read static/cached data or alternatively you can make the application itself dynamic by introducing something dynamic inside a Suspense boundary. If you use a Synchronously dynamic API like the one mentioned above you will likely get incomplete parts of your component tree identified as dynamic b/c the synchronous dynamic API immediately terminates the prerender. We provide component stacks in these cases but the correct fix is to find the synchronous dynamic API and make it async. |
||
|
|
86abd1c969 |
Add connection() as a new dynamic API (#69949)
In https://github.com/vercel/next.js/pull/68812 I updated most dynamic APIs to be async. One API that was not udpated was `unstable_noStore()`. This API is marked as unstable and doesn't quite fit the semantics we're exploring with dynamicIO and partial prerendering and so rather than converting it to be async we're going to deprecate it and replace it with an entirely new API. This PR doesn't actually deprecate anything yet but it does introduce `connection()`. The idea with `connection` is that you are waiting until there is a real user Request before proceeding. In the context of prerendering no Request will ever happen so the page cannot produce a static result. (this is similar to how `unstable_noStore()` works today). In a PPR context the currently rendering component won't resolve but a parent Suspense boundary can still statically render a fallback. `connect()` returns a `Promise<void>`. It is tempting to call the API `request()` and return a `Promise<Request>` however we have to guard access to the underlying Request carefully to ensure we can maximally prerender pages and to avoid confusion and maybe some frustration we are naming it `connection` since this doesn't imply a specific data set that might be returned. ``` import { connection } from 'next/server' async function MyServerComponent() { await connection() // everthing after this point will be excluded from prerendering const rand = Math.random() return <span>{rand}</span> } ``` |
||
|
|
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.
|
||
|
|
fe8d953e2d |
Replace hasRedbox() (#67025)
...with `assertHasRedbox` and `assertNoRedbox`. `hasRedbox()` has a hardcoded timeout of 5s that is only required for the negative assertion. Instead, we now have dedicated assertions for the positive (`assertHasRedbox`) and negative case (`assertNoRedbox`). The negative assertion still has the hardcoded timeout. But the positive assertion just retries until we find the Redbox. This speeds up tests using the positive assertion. Removing `hasRedbox` also uncovered some unused expressions e.g. `await hasRedbox(browser)`. These expressions probably wanted to use `expect(await hasRedbox(browser)).toBe(true) |
||
|
|
734fa1df62 |
fix: ensure route handlers properly track dynamic access (#66446)
In #60645, dynamic access tracking was refactored but we erroneously stopped tracking dynamic access in route handlers. Request proxying, as well as tracking segment level configs (such as `export const dynamic = 'force-dynamic'`), were only enabled during static generation. This was an unintended breaking change that consequently caused dynamic access to not properly bail from data cache in various circumstances. This adds some more rigorous testing for route handlers, as this seems to be a fairly large gap in our fetch cache testing currently. This PR is easiest to review with [whitespace disabled](https://github.com/vercel/next.js/pull/66446/files?w=1). |
||
|
|
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 |
||
|
|
ea56c8f7ad |
DX: add route context to dynamic errors for app routes (#62844)
Gives the users pathname context on routes that access Dynamic API's so that if these errors are caught they can modify their code accordingly. This is a followup to #61332. Closes NEXT-2695 |
||
|
|
3008af6b0e |
DX: add route context to the dynamic errors (#61332)
### What Given user infomation when the dynamic errors are thrown, e.g. bad `cookies` or `headers` usages. Now users can tell through the error information to see which pathname is broken, and trace down the usage. #### before ``` Page couldn't be rendered statically because ... This page needs to bail out of prerendering at this point because ... ``` #### after ``` Route /cookies couldn't be rendered statically because ... Route /server needs to bail out of prerendering at this point because ... ``` ### Why When you have multi pages in your app, such as 100+, and many page might uses these. This is hard to trace down where exactly the error is from Closes NEXT-2283 Cloese NEXT-2265 |
||
|
|
b5772b859a |
Dynamic APIs (#60645)
formalizes the concept of dynamic APIs inside Next to allow for varying semantics beyond just staticGenerationBailout. ### Dynamic APIs #### `markCurrentScopeAsDynamic` useful to bail out of default caching semantics but does not imply a Request specific data source was read. critically, this semantic is ignored if you are inside a cache scope #### `trackDynamicDataAccessed` Must be called before reading any data source that is derived from Request specific data. Currently this is `cookies()`, `headers()`, and `searchParams`. This kind of data access inside a cache scope is forbidden (it always should have been, but now it will error). #### `trackDynamicFetch` This one is unideal but the complexity of patch-fetch's current implementation necessitates it for now. Essentially it will postpone if we are prerendering. Long term this should be eliminated with a refactor of patch fetch. ### Other Improvements Also removes the `staticGenerationBailout` implementation as it has been replaced with more specific logic in the places it was previously being used. One area that has also been enhanced is the proxy for app-route modules. Previously we proxied the Request every time however when we are doing non-static generation executions we generally don't want the overhead of wrapping the request. In the refactor here I also improved the runtime performance by using static proxy handlers and I believe I also fixed a few bugs related to `clone` and `url` In general there has been a bit of refactoring to clarify how we should handle various render/execution states and a reduction in implicit side effects for proper execution. Another callout to notice is that app-route modules do not attempt a static generation if they are force-dynamic regardless of the PPR setting. Previously the PPR setting would opt them into this code path which is not necessary because PPR itself does not work for routes, only pages. Closes NEXT-2099 |