first commit
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

View File

@@ -0,0 +1,365 @@
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
import * as nodePath from 'node:path'
import type { Playwright } from '../../../lib/next-webdriver'
describe.each([
{
description: 'without runtime prefetch configs',
hasRuntimePrefetch: false,
fixturePath: 'fixtures/without-prefetch-config',
},
{
description: 'with runtime prefetch configs',
hasRuntimePrefetch: true,
fixturePath: 'fixtures/with-prefetch-config',
},
])(
'cache-components-dev-warmup - $description',
({ fixturePath, hasRuntimePrefetch }) => {
const { next, isTurbopack } = nextTestSetup({
files: nodePath.join(__dirname, fixturePath),
})
// Restart the dev server for each test to clear the in-memory cache.
// We're testing cache-warming behavior here, so we don't want tests to interfere with each other.
let isFirstTest = true
beforeEach(async () => {
if (isFirstTest) {
// There's no point restarting if this is the first test.
isFirstTest = false
return
}
await next.stop()
await next.clean()
await next.start()
})
function assertLog(
logs: Array<{ source: string; message: string }>,
message: string,
expectedEnvironment: string
) {
// Match logs that contain the message, with any environment.
const logPattern = new RegExp(
`^(?=.*\\b${message}\\b)(?=.*\\b(Cache|Prerender|Prefetch|Prefetchable|Server)\\b).*`
)
const logMessages = logs.map((log) => log.message)
const messages = logMessages.filter((message) => logPattern.test(message))
// If there's zero or more than one logs that match, the test is not set up correctly.
if (messages.length === 0) {
throw new Error(
`Found no logs matching '${message}':\n\n${logMessages.map((s, i) => `${i}. ${s}`).join('\n')}}`
)
}
if (messages.length > 1) {
throw new Error(
`Found multiple logs matching '${message}':\n\n${messages.map((s, i) => `${i}. ${s}`).join('\n')}`
)
}
// The message should have the expected environment.
const actualMessageText = messages[0]
const [, actualEnvironment] = actualMessageText.match(logPattern)!
expect([actualEnvironment, actualMessageText]).toEqual([
expectedEnvironment,
expect.stringContaining(message),
])
}
async function testInitialLoad(
path: string,
assertLogs: (browser: Playwright) => Promise<void>
) {
const browser = await next.browser(path)
// Initial load.
await retry(() => assertLogs(browser))
// We should not see any errors related to the aborted render.
expect(next.cliOutput).not.toContain(
'AbortError: This operation was aborted'
)
// After another load (with warm caches) the logs should be the same.
await browser.loadPage(next.url + path) // clears old logs
await retry(() => assertLogs(browser))
expect(next.cliOutput).not.toContain(
'AbortError: This operation was aborted'
)
if (isTurbopack) {
// FIXME:
// In Turbopack, requests to the /revalidate route seem to occasionally crash
// due to some HMR or compilation issue. `revalidatePath` throws this error:
//
// Invariant: static generation store missing in revalidatePath <path>
//
// This is unrelated to the logic being tested here, so for now, we skip the assertions
// that require us to revalidate.
console.log('WARNING: skipping revalidation assertions in turbopack')
return
}
// After a revalidation the subsequent warmup render must discard stale
// cache entries.
// This should not affect the environment labels.
await revalidatePath(path)
await browser.loadPage(next.url + path) // clears old logs
await retry(() => assertLogs(browser))
// We should not see any errors related to the aborted render.
expect(next.cliOutput).not.toContain(
'AbortError: This operation was aborted'
)
}
async function testNavigation(
path: string,
assertLogs: (browser: Playwright) => Promise<void>
) {
const browser = await next.browser('/')
// Initial nav (first time loading the page)
await browser.elementByCss(`a[href="${path}"]`).click()
await retry(() => assertLogs(browser))
// We should not see any errors related to the aborted render.
expect(next.cliOutput).not.toContain(
'AbortError: This operation was aborted'
)
// Reload, and perform another nav (with warm caches). the logs should be the same.
await browser.loadPage(next.url + '/') // clears old logs
await browser.elementByCss(`a[href="${path}"]`).click()
await retry(() => assertLogs(browser))
expect(next.cliOutput).not.toContain(
'AbortError: This operation was aborted'
)
if (isTurbopack) {
// FIXME:
// In Turbopack, requests to the /revalidate route seem to occasionally crash
// due to some HMR or compilation issue. `revalidatePath` throws this error:
//
// Invariant: static generation store missing in revalidatePath <path>
//
// This is unrelated to the logic being tested here, so for now, we skip the assertions
// that require us to revalidate.
console.log('WARNING: skipping revalidation assertions in turbopack')
return
}
// After a revalidation the subsequent warmup render must discard stale
// cache entries.
// This should not affect the environment labels.
await revalidatePath(path)
await browser.loadPage(next.url + '/') // clears old logs
await browser.elementByCss(`a[href="${path}"]`).click()
await retry(() => assertLogs(browser))
expect(next.cliOutput).not.toContain(
'AbortError: This operation was aborted'
)
}
async function revalidatePath(path: string) {
const response = await next.fetch(
`/revalidate?path=${encodeURIComponent(path)}`
)
if (!response.ok) {
throw new Error(
`Failed to revalidate path: '${path}' - server responded with status ${response.status}`
)
}
}
const RUNTIME_ENV = hasRuntimePrefetch ? 'Prefetch' : 'Prefetchable'
describe.each([
{ description: 'initial load', isInitialLoad: true },
{ description: 'navigation', isInitialLoad: false },
])('$description', ({ isInitialLoad }) => {
describe('cached data resolves in the correct phase', () => {
it('cached data + cached fetch', async () => {
const path = '/simple'
const assertLogs = async (browser: Playwright) => {
const logs = await browser.log()
assertLog(logs, 'after cache read - layout', 'Prerender')
assertLog(logs, 'after cache read - page', 'Prerender')
assertLog(logs, 'after successive cache reads - page', 'Prerender')
assertLog(logs, 'after cached fetch - layout', 'Prerender')
assertLog(logs, 'after cached fetch - page', 'Prerender')
assertLog(logs, 'after uncached fetch - layout', 'Server')
assertLog(logs, 'after uncached fetch - page', 'Server')
}
if (isInitialLoad) {
await testInitialLoad(path, assertLogs)
} else {
await testNavigation(path, assertLogs)
}
})
it('cached data + private cache', async () => {
const path = '/private-cache'
const assertLogs = async (browser: Playwright) => {
const logs = await browser.log()
assertLog(logs, 'after cache read - layout', 'Prerender')
assertLog(logs, 'after cache read - page', 'Prerender')
// Private caches are dynamic holes in static prerenders,
// so they shouldn't resolve in the static stage.
assertLog(logs, 'after private cache read - page', RUNTIME_ENV)
assertLog(logs, 'after private cache read - layout', RUNTIME_ENV)
assertLog(
logs,
'after successive private cache reads - page',
RUNTIME_ENV
)
assertLog(logs, 'after uncached fetch - layout', 'Server')
assertLog(logs, 'after uncached fetch - page', 'Server')
}
if (isInitialLoad) {
await testInitialLoad(path, assertLogs)
} else {
await testNavigation(path, assertLogs)
}
})
it('cached data + short-lived cached data', async () => {
const path = '/short-lived-cache'
const assertLogs = async (browser: Playwright) => {
const logs = await browser.log()
assertLog(logs, 'after cache read - layout', 'Prerender')
assertLog(logs, 'after cache read - page', 'Prerender')
// Short lived caches are dynamic holes in static prerenders,
// so they shouldn't resolve in the static stage.
assertLog(logs, 'after short-lived cache read - page', RUNTIME_ENV)
assertLog(
logs,
'after short-lived cache read - layout',
RUNTIME_ENV
)
assertLog(logs, 'after uncached fetch - layout', 'Server')
assertLog(logs, 'after uncached fetch - page', 'Server')
}
if (isInitialLoad) {
await testInitialLoad(path, assertLogs)
} else {
await testNavigation(path, assertLogs)
}
})
it('cache reads that reveal more components with more caches', async () => {
const path = '/successive-caches'
const assertLogs = async (browser: Playwright) => {
const logs = await browser.log()
// No matter how deeply we nest the component tree,
// if all the IO is cached, it should be labeled as Prerender.
assertLog(logs, 'after cache 1', 'Prerender')
assertLog(logs, 'after cache 2', 'Prerender')
assertLog(logs, 'after caches 1 and 2', 'Prerender')
assertLog(logs, 'after cache 3', 'Prerender')
}
if (isInitialLoad) {
await testInitialLoad(path, assertLogs)
} else {
await testNavigation(path, assertLogs)
}
})
})
it('request APIs resolve in the correct phase', async () => {
const path = '/apis/123'
const assertLogs = async (browser: Playwright) => {
const logs = await browser.log()
assertLog(logs, 'after cache read - page', 'Prerender')
// TODO: we should only label this as "Prefetch" if there's a prefetch config.
assertLog(logs, `after cookies`, RUNTIME_ENV)
assertLog(logs, `after headers`, RUNTIME_ENV)
assertLog(logs, `after params`, RUNTIME_ENV)
assertLog(logs, `after searchParams`, RUNTIME_ENV)
assertLog(logs, 'after connection', 'Server')
}
if (isInitialLoad) {
await testInitialLoad(path, assertLogs)
} else {
await testNavigation(path, assertLogs)
}
})
// FIXME: it seems like in Turbopack we sometimes get two instances of `workUnitAsyncStorage` --
// `app-render` gets a second, newer instance, different from `io()`.
// Thus, `io()` gets an undefined `workUnitStore` and does nothing, so sync IO does not get tracked at all.
// This is likely caused by the same bug that breaks `/revalidate` (see other FIXME above),
// where a route crashes due to a missing `workStore`.
if (!isTurbopack) {
it('sync IO in the static phase', async () => {
const path = '/sync-io/static'
const assertLogs = async (browser: Playwright) => {
const logs = await browser.log()
assertLog(logs, 'after first cache', 'Prerender')
// sync IO in the static stage errors and advances to Server.
assertLog(logs, 'after sync io', 'Server')
assertLog(logs, 'after cache read - page', 'Server')
}
if (isInitialLoad) {
await testInitialLoad(path, assertLogs)
} else {
await testNavigation(path, assertLogs)
}
})
it('sync IO in the runtime phase', async () => {
const path = '/sync-io/runtime'
const assertLogs = async (browser: Playwright) => {
const logs = await browser.log()
assertLog(logs, 'after first cache', 'Prerender')
assertLog(logs, 'after cookies', RUNTIME_ENV)
if (hasRuntimePrefetch) {
// if runtime prefetching is on, sync IO in the runtime stage errors and advances to Server.
assertLog(logs, 'after sync io', 'Server')
assertLog(logs, 'after cache read - page', 'Server')
} else {
// if runtime prefetching is not on, sync IO in the runtime stage does nothing.
assertLog(logs, 'after sync io', RUNTIME_ENV)
assertLog(logs, 'after cache read - page', RUNTIME_ENV)
}
}
if (isInitialLoad) {
await testInitialLoad(path, assertLogs)
} else {
await testNavigation(path, assertLogs)
}
})
}
})
}
)

View File

@@ -0,0 +1,49 @@
import { cookies, headers } from 'next/headers'
import { CachedData } from '../../data-fetching'
import { connection } from 'next/server'
import { Suspense } from 'react'
export const unstable_instant = { prefetch: 'runtime', samples: [{}] }
const CACHE_KEY = __dirname + '/__PAGE__'
export default function Page({ params, searchParams }) {
return (
<main>
<p>
This page checks whether runtime/dynamic APIs resolve in the correct
stage (regardless of whether we had a cache miss or not)
</p>
<CachedData cacheKey={CACHE_KEY} label="page" />
<LogAfter label="--- dynamic stage ---" api={() => connection()} />
{/* Runtime */}
<LogAfter label="cookies" api={() => cookies()} />
<LogAfter label="headers" api={() => headers()} />
<LogAfter label="params" api={() => params} />
<LogAfter label="searchParams" api={() => searchParams} />
{/* Dynamic */}
<LogAfter label="connection" api={() => connection()} />
</main>
)
}
function LogAfter({ label, api }: { label: string; api: () => Promise<any> }) {
return (
<Suspense fallback={<div>Waiting for {label}...</div>}>
<LogAfterInner label={label} api={api} />
</Suspense>
)
}
async function LogAfterInner({
label,
api,
}: {
label: string
api: () => Promise<any>
}) {
await api()
console.log(`after ${label}`)
return <div>Finished {label}</div>
}

View File

@@ -0,0 +1,103 @@
export async function fetchCachedRandom(cacheKey: string) {
return fetchCached(
`https://next-data-api-endpoint.vercel.app/api/random?key=${encodeURIComponent('cached-' + cacheKey)}`
)
}
export async function fetchCached(url: string) {
const response = await fetch(url, { cache: 'force-cache' })
return response.text()
}
export async function getCachedData(_key: string) {
'use cache'
await new Promise((r) => setTimeout(r))
return Math.random()
}
export async function CachedData({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
const data = await getCachedData(cacheKey)
console.log(`after cache read - ${label}`)
return (
<dl>
<dt>Cached Data</dt>
<dd>{data}</dd>
</dl>
)
}
export async function SuccessiveCachedData({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
// This components tests if we correctly handle the case where resolving a cache
// reveals another cache in the children. When we're filling caches, we should fill both.
const data1 = await getCachedData(`${cacheKey}-successive-1`)
return (
<dl>
<dt>Cached Data (successive reads)</dt>
<dd>{data1}</dd>
<dd>
<SuccessiveCachedDataChild label={label} cacheKey={cacheKey} />
</dd>
</dl>
)
}
async function SuccessiveCachedDataChild({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
const data2 = await getCachedData(`${cacheKey}-successive-2`)
console.log(`after successive cache reads - ${label}`)
return <>{data2}</>
}
export async function CachedFetch({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
const data = await fetchCachedRandom(cacheKey)
console.log(`after cached fetch - ${label}`)
return (
<dl>
<dt>Cached Fetch</dt>
<dd>{data}</dd>
</dl>
)
}
export async function UncachedFetch({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
const response = await fetch(
`https://next-data-api-endpoint.vercel.app/api/random?key=${encodeURIComponent('uncached-' + cacheKey)}`
)
console.log(`after uncached fetch - ${label}`)
const data = await response.text()
return (
<dl>
<dt>Uncached Fetch</dt>
<dd>{data}</dd>
</dl>
)
}

View File

@@ -0,0 +1,7 @@
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,32 @@
import Link from 'next/link'
export default function Page() {
// NOTE: these links must be kept in sync with `path` variables used in the test
return (
<main>
<ul>
<li>
<Link href="/simple">/simple</Link>
</li>
<li>
<Link href="/private-cache">/private-cache</Link>
</li>
<li>
<Link href="/short-lived-cache">/short-lived-cache</Link>
</li>
<li>
<Link href="/successive-caches">/successive-caches</Link>
</li>
<li>
<Link href="/apis/123">/apis/123</Link>
</li>
<li>
<Link href="/sync-io/static">/sync-io/static</Link>
</li>
<li>
<Link href="/sync-io/runtime">/sync-io/runtime</Link>
</li>
</ul>
</main>
)
}

View File

@@ -0,0 +1,55 @@
export async function PrivateCachedData({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
const data = await getPrivateCachedData(cacheKey)
console.log(`after private cache read - ${label}`)
return (
<dl>
<dt>Private Cached Data (Page)</dt>
<dd>{data}</dd>
</dl>
)
}
export async function SuccessivePrivateCachedData({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
// This components tests if we correctly handle the case where resolving a cache
// reveals another cache in the children. When we're filling caches, we should fill both.
const data1 = await getPrivateCachedData(`${cacheKey}-successive-1`)
return (
<dl>
<dt>Private Cached Data (successive reads)</dt>
<dd>{data1}</dd>
<dd>
<SuccessivePrivateCachedDataChild label={label} cacheKey={cacheKey} />
</dd>
</dl>
)
}
async function SuccessivePrivateCachedDataChild({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
const data2 = await getPrivateCachedData(`${cacheKey}-successive-2`)
console.log(`after successive private cache reads - ${label}`)
return <>{data2}</>
}
async function getPrivateCachedData(_key: string) {
'use cache: private'
await new Promise((r) => setTimeout(r))
return Math.random()
}

View File

@@ -0,0 +1,29 @@
import { Suspense } from 'react'
import { UncachedFetch, CachedData } from '../data-fetching'
import { PrivateCachedData } from './data-fetching'
export const unstable_instant = { prefetch: 'runtime', samples: [{}] }
const CACHE_KEY = '/private-cache/__LAYOUT__'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<section>
<h1>Layout</h1>
<p>This data is from a layout</p>
<CachedData label="layout" cacheKey={CACHE_KEY} />
<Suspense fallback="Loading private cache...">
<PrivateCachedData label="layout" cacheKey={CACHE_KEY} />
</Suspense>
<Suspense fallback="Loading uncached fetch...">
<UncachedFetch label="layout" cacheKey={CACHE_KEY} />
</Suspense>
</section>
</>
)
}

View File

@@ -0,0 +1,27 @@
import { Suspense } from 'react'
import { CachedData, UncachedFetch } from '../data-fetching'
import { PrivateCachedData, SuccessivePrivateCachedData } from './data-fetching'
const CACHE_KEY = '/private-cache/__PAGE__'
export default async function Page() {
return (
<main>
<h1>Warmup Dev Renders - private cache</h1>
<CachedData label="page" cacheKey={CACHE_KEY} />
<Suspense fallback="Loading private cache...">
<PrivateCachedData label="page" cacheKey={CACHE_KEY} />
</Suspense>
<Suspense fallback="Loading two successive private caches...">
<SuccessivePrivateCachedData label="page" cacheKey={CACHE_KEY} />
</Suspense>
<Suspense fallback="Loading uncached fetch...">
<UncachedFetch label="page" cacheKey={CACHE_KEY} />
</Suspense>
</main>
)
}

View File

@@ -0,0 +1,8 @@
import { revalidatePath } from 'next/cache'
export async function GET(request: Request) {
const path = new URL(request.url).searchParams.get('path')!
revalidatePath(path)
return Response.json({ revalidated: true })
}

View File

@@ -0,0 +1,25 @@
import { cacheLife } from 'next/cache'
export async function ShortLivedCache({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
const data = await getShortLivedCachedData(cacheKey)
console.log(`after short-lived cache read - ${label}`)
return (
<dl>
<dt>Short-lived Cached Data (Page)</dt>
<dd>{data}</dd>
</dl>
)
}
async function getShortLivedCachedData(_key: string) {
'use cache'
cacheLife('seconds')
await new Promise((r) => setTimeout(r))
return Math.random()
}

View File

@@ -0,0 +1,29 @@
import { Suspense } from 'react'
import { UncachedFetch, CachedData } from '../data-fetching'
import { ShortLivedCache } from './data-fetching'
export const unstable_instant = { prefetch: 'runtime', samples: [{}] }
const CACHE_KEY = __dirname + '/__LAYOUT__'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<section>
<h1>Layout</h1>
<p>This data is from a layout</p>
<CachedData label="layout" cacheKey={CACHE_KEY} />
<Suspense fallback="Loading short-lived cache...">
<ShortLivedCache label="layout" cacheKey={CACHE_KEY} />
</Suspense>
<Suspense fallback="Loading uncached fetch...">
<UncachedFetch label="layout" cacheKey={CACHE_KEY} />
</Suspense>
</section>
</>
)
}

View File

@@ -0,0 +1,23 @@
import { Suspense } from 'react'
import { CachedData, UncachedFetch } from '../data-fetching'
import { ShortLivedCache } from './data-fetching'
const CACHE_KEY = __dirname + '/__PAGE__'
export default async function Page() {
return (
<main>
<h1>Warmup Dev Renders - short lived cache</h1>
<CachedData label="page" cacheKey={CACHE_KEY} />
<Suspense fallback="Loading short-lived cache...">
<ShortLivedCache label="page" cacheKey={CACHE_KEY} />
</Suspense>
<Suspense fallback="Loading uncached fetch...">
<UncachedFetch label="page" cacheKey={CACHE_KEY} />
</Suspense>
</main>
)
}

View File

@@ -0,0 +1,26 @@
import { Suspense } from 'react'
import { UncachedFetch, CachedFetch, CachedData } from '../data-fetching'
export const unstable_instant = { prefetch: 'runtime', samples: [{}] }
const CACHE_KEY = __dirname + '/__LAYOUT__'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<section>
<h1>Layout</h1>
<p>This data is from a layout</p>
<CachedData label="layout" cacheKey={CACHE_KEY} />
<CachedFetch label="layout" cacheKey={CACHE_KEY} />
<Suspense fallback="Loading uncached fetch...">
<UncachedFetch label="layout" cacheKey={CACHE_KEY} />
</Suspense>
</section>
</>
)
}

View File

@@ -0,0 +1,10 @@
import { fetchCachedRandom, getCachedData } from '../data-fetching'
// Deliberately using the same cache keys as the page.
const CACHE_KEY = __dirname + '/__PAGE__'
export default async function Loading() {
await fetchCachedRandom(CACHE_KEY) // Mirrors `CachedFetchingComponent`
await getCachedData(CACHE_KEY) // Mirrors `CachedDataComponent`
return <main>loading...</main>
}

View File

@@ -0,0 +1,26 @@
import { Suspense } from 'react'
import {
CachedData,
CachedFetch,
SuccessiveCachedData,
UncachedFetch,
} from '../data-fetching'
const CACHE_KEY = __dirname + '/__PAGE__'
export default async function Page() {
return (
<main>
<h1>Warmup Dev Renders</h1>
<CachedData label="page" cacheKey={CACHE_KEY} />
<SuccessiveCachedData label="page" cacheKey={CACHE_KEY} />
<CachedFetch label="page" cacheKey={CACHE_KEY} />
<Suspense fallback="Loading uncached fetch...">
<UncachedFetch label="page" cacheKey={CACHE_KEY} />
</Suspense>
</main>
)
}

View File

@@ -0,0 +1,46 @@
export const unstable_instant = { prefetch: 'runtime', samples: [{}] }
export default async function Page() {
return (
<main>
<h1>Warmup Dev Renders - deep successive cache reads</h1>
<One />
</main>
)
}
async function One() {
const cache1 = await fastCache()
console.log('after cache 1')
return <Two cache1={cache1} />
}
async function Two({ cache1 }) {
const cache2 = await slowCache(1)
console.log('after cache 2')
return <Three cache1={cache1} cache2={cache2} />
}
async function Three({ cache1, cache2 }) {
console.log('after caches 1 and 2')
const cache3 = await slowCache(2)
console.log('after cache 3')
return (
<div>
<div>Cache 1: {cache1}</div>
<div>Cache 2: {cache2}</div>
<div>Cache 3: {cache3}</div>
</div>
)
}
async function fastCache() {
'use cache'
return Math.random()
}
async function slowCache(_key: number) {
'use cache'
await new Promise((resolve) => setTimeout(resolve))
return Math.random()
}

View File

@@ -0,0 +1,31 @@
import { Suspense } from 'react'
import { CachedData, getCachedData } from '../../data-fetching'
import { cookies } from 'next/headers'
export const unstable_instant = { prefetch: 'runtime', samples: [{}] }
const CACHE_KEY = __dirname + '/__PAGE__'
export default async function Page() {
return (
<main>
<h1>Sync IO - runtime stage</h1>
<Suspense fallback={<div>Loading...</div>}>
<Runtime />
</Suspense>
</main>
)
}
async function Runtime() {
await getCachedData(CACHE_KEY + '-1')
console.log(`after first cache`)
await cookies()
console.log(`after cookies`)
Date.now()
console.log(`after sync io`)
return <CachedData label="page" cacheKey={CACHE_KEY} />
}

View File

@@ -0,0 +1,20 @@
import { CachedData, getCachedData } from '../../data-fetching'
export const unstable_instant = { prefetch: 'runtime', samples: [{}] }
const CACHE_KEY = __dirname + '/__PAGE__'
export default async function Page() {
await getCachedData(CACHE_KEY + '-1')
console.log(`after first cache`)
Date.now()
console.log(`after sync io`)
return (
<main>
<h1>Sync IO - static stage</h1>
<CachedData label="page" cacheKey={CACHE_KEY} />
</main>
)
}

View File

@@ -0,0 +1,7 @@
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig

View File

@@ -0,0 +1,47 @@
import { cookies, headers } from 'next/headers'
import { CachedData } from '../../data-fetching'
import { connection } from 'next/server'
import { Suspense } from 'react'
const CACHE_KEY = __dirname + '/__PAGE__'
export default function Page({ params, searchParams }) {
return (
<main>
<p>
This page checks whether runtime/dynamic APIs resolve in the correct
stage (regardless of whether we had a cache miss or not)
</p>
<CachedData cacheKey={CACHE_KEY} label="page" />
<LogAfter label="--- dynamic stage ---" api={() => connection()} />
{/* Runtime */}
<LogAfter label="cookies" api={() => cookies()} />
<LogAfter label="headers" api={() => headers()} />
<LogAfter label="params" api={() => params} />
<LogAfter label="searchParams" api={() => searchParams} />
{/* Dynamic */}
<LogAfter label="connection" api={() => connection()} />
</main>
)
}
function LogAfter({ label, api }: { label: string; api: () => Promise<any> }) {
return (
<Suspense fallback={<div>Waiting for {label}...</div>}>
<LogAfterInner label={label} api={api} />
</Suspense>
)
}
async function LogAfterInner({
label,
api,
}: {
label: string
api: () => Promise<any>
}) {
await api()
console.log(`after ${label}`)
return <div>Finished {label}</div>
}

View File

@@ -0,0 +1,103 @@
export async function fetchCachedRandom(cacheKey: string) {
return fetchCached(
`https://next-data-api-endpoint.vercel.app/api/random?key=${encodeURIComponent('cached-' + cacheKey)}`
)
}
export async function fetchCached(url: string) {
const response = await fetch(url, { cache: 'force-cache' })
return response.text()
}
export async function getCachedData(_key: string) {
'use cache'
await new Promise((r) => setTimeout(r))
return Math.random()
}
export async function CachedData({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
const data = await getCachedData(cacheKey)
console.log(`after cache read - ${label}`)
return (
<dl>
<dt>Cached Data</dt>
<dd>{data}</dd>
</dl>
)
}
export async function SuccessiveCachedData({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
// This components tests if we correctly handle the case where resolving a cache
// reveals another cache in the children. When we're filling caches, we should fill both.
const data1 = await getCachedData(`${cacheKey}-successive-1`)
return (
<dl>
<dt>Cached Data (successive reads)</dt>
<dd>{data1}</dd>
<dd>
<SuccessiveCachedDataChild label={label} cacheKey={cacheKey} />
</dd>
</dl>
)
}
async function SuccessiveCachedDataChild({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
const data2 = await getCachedData(`${cacheKey}-successive-2`)
console.log(`after successive cache reads - ${label}`)
return <>{data2}</>
}
export async function CachedFetch({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
const data = await fetchCachedRandom(cacheKey)
console.log(`after cached fetch - ${label}`)
return (
<dl>
<dt>Cached Fetch</dt>
<dd>{data}</dd>
</dl>
)
}
export async function UncachedFetch({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
const response = await fetch(
`https://next-data-api-endpoint.vercel.app/api/random?key=${encodeURIComponent('uncached-' + cacheKey)}`
)
console.log(`after uncached fetch - ${label}`)
const data = await response.text()
return (
<dl>
<dt>Uncached Fetch</dt>
<dd>{data}</dd>
</dl>
)
}

View File

@@ -0,0 +1,7 @@
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}

View File

@@ -0,0 +1,32 @@
import Link from 'next/link'
export default function Page() {
// NOTE: these links must be kept in sync with `path` variables used in the test
return (
<main>
<ul>
<li>
<Link href="/simple">/simple</Link>
</li>
<li>
<Link href="/private-cache">/private-cache</Link>
</li>
<li>
<Link href="/short-lived-cache">/short-lived-cache</Link>
</li>
<li>
<Link href="/successive-caches">/successive-caches</Link>
</li>
<li>
<Link href="/apis/123">/apis/123</Link>
</li>
<li>
<Link href="/sync-io/static">/sync-io/static</Link>
</li>
<li>
<Link href="/sync-io/runtime">/sync-io/runtime</Link>
</li>
</ul>
</main>
)
}

View File

@@ -0,0 +1,55 @@
export async function PrivateCachedData({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
const data = await getPrivateCachedData(cacheKey)
console.log(`after private cache read - ${label}`)
return (
<dl>
<dt>Private Cached Data (Page)</dt>
<dd>{data}</dd>
</dl>
)
}
export async function SuccessivePrivateCachedData({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
// This components tests if we correctly handle the case where resolving a cache
// reveals another cache in the children. When we're filling caches, we should fill both.
const data1 = await getPrivateCachedData(`${cacheKey}-successive-1`)
return (
<dl>
<dt>Private Cached Data (successive reads)</dt>
<dd>{data1}</dd>
<dd>
<SuccessivePrivateCachedDataChild label={label} cacheKey={cacheKey} />
</dd>
</dl>
)
}
async function SuccessivePrivateCachedDataChild({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
const data2 = await getPrivateCachedData(`${cacheKey}-successive-2`)
console.log(`after successive private cache reads - ${label}`)
return <>{data2}</>
}
async function getPrivateCachedData(_key: string) {
'use cache: private'
await new Promise((r) => setTimeout(r))
return Math.random()
}

View File

@@ -0,0 +1,27 @@
import { Suspense } from 'react'
import { UncachedFetch, CachedData } from '../data-fetching'
import { PrivateCachedData } from './data-fetching'
const CACHE_KEY = '/private-cache/__LAYOUT__'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<section>
<h1>Layout</h1>
<p>This data is from a layout</p>
<CachedData label="layout" cacheKey={CACHE_KEY} />
<Suspense fallback="Loading private cache...">
<PrivateCachedData label="layout" cacheKey={CACHE_KEY} />
</Suspense>
<Suspense fallback="Loading uncached fetch...">
<UncachedFetch label="layout" cacheKey={CACHE_KEY} />
</Suspense>
</section>
</>
)
}

View File

@@ -0,0 +1,27 @@
import { Suspense } from 'react'
import { CachedData, UncachedFetch } from '../data-fetching'
import { PrivateCachedData, SuccessivePrivateCachedData } from './data-fetching'
const CACHE_KEY = '/private-cache/__PAGE__'
export default async function Page() {
return (
<main>
<h1>Warmup Dev Renders - private cache</h1>
<CachedData label="page" cacheKey={CACHE_KEY} />
<Suspense fallback="Loading private cache...">
<PrivateCachedData label="page" cacheKey={CACHE_KEY} />
</Suspense>
<Suspense fallback="Loading two successive private caches...">
<SuccessivePrivateCachedData label="page" cacheKey={CACHE_KEY} />
</Suspense>
<Suspense fallback="Loading uncached fetch...">
<UncachedFetch label="page" cacheKey={CACHE_KEY} />
</Suspense>
</main>
)
}

View File

@@ -0,0 +1,8 @@
import { revalidatePath } from 'next/cache'
export async function GET(request: Request) {
const path = new URL(request.url).searchParams.get('path')!
revalidatePath(path)
return Response.json({ revalidated: true })
}

View File

@@ -0,0 +1,25 @@
import { cacheLife } from 'next/cache'
export async function ShortLivedCache({
label,
cacheKey,
}: {
label: string
cacheKey: string
}) {
const data = await getShortLivedCachedData(cacheKey)
console.log(`after short-lived cache read - ${label}`)
return (
<dl>
<dt>Short-lived Cached Data (Page)</dt>
<dd>{data}</dd>
</dl>
)
}
async function getShortLivedCachedData(_key: string) {
'use cache'
cacheLife('seconds')
await new Promise((r) => setTimeout(r))
return Math.random()
}

View File

@@ -0,0 +1,27 @@
import { Suspense } from 'react'
import { UncachedFetch, CachedData } from '../data-fetching'
import { ShortLivedCache } from './data-fetching'
const CACHE_KEY = __dirname + '/__LAYOUT__'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<section>
<h1>Layout</h1>
<p>This data is from a layout</p>
<CachedData label="layout" cacheKey={CACHE_KEY} />
<Suspense fallback="Loading short-lived cache...">
<ShortLivedCache label="layout" cacheKey={CACHE_KEY} />
</Suspense>
<Suspense fallback="Loading uncached fetch...">
<UncachedFetch label="layout" cacheKey={CACHE_KEY} />
</Suspense>
</section>
</>
)
}

View File

@@ -0,0 +1,23 @@
import { Suspense } from 'react'
import { CachedData, UncachedFetch } from '../data-fetching'
import { ShortLivedCache } from './data-fetching'
const CACHE_KEY = __dirname + '/__PAGE__'
export default async function Page() {
return (
<main>
<h1>Warmup Dev Renders - short lived cache</h1>
<CachedData label="page" cacheKey={CACHE_KEY} />
<Suspense fallback="Loading short-lived cache...">
<ShortLivedCache label="page" cacheKey={CACHE_KEY} />
</Suspense>
<Suspense fallback="Loading uncached fetch...">
<UncachedFetch label="page" cacheKey={CACHE_KEY} />
</Suspense>
</main>
)
}

View File

@@ -0,0 +1,24 @@
import { Suspense } from 'react'
import { UncachedFetch, CachedFetch, CachedData } from '../data-fetching'
const CACHE_KEY = __dirname + '/__LAYOUT__'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
{children}
<section>
<h1>Layout</h1>
<p>This data is from a layout</p>
<CachedData label="layout" cacheKey={CACHE_KEY} />
<CachedFetch label="layout" cacheKey={CACHE_KEY} />
<Suspense fallback="Loading uncached fetch...">
<UncachedFetch label="layout" cacheKey={CACHE_KEY} />
</Suspense>
</section>
</>
)
}

View File

@@ -0,0 +1,10 @@
import { fetchCachedRandom, getCachedData } from '../data-fetching'
// Deliberately using the same cache keys as the page.
const CACHE_KEY = __dirname + '/__PAGE__'
export default async function Loading() {
await fetchCachedRandom(CACHE_KEY) // Mirrors `CachedFetchingComponent`
await getCachedData(CACHE_KEY) // Mirrors `CachedDataComponent`
return <main>loading...</main>
}

View File

@@ -0,0 +1,26 @@
import { Suspense } from 'react'
import {
CachedData,
CachedFetch,
SuccessiveCachedData,
UncachedFetch,
} from '../data-fetching'
const CACHE_KEY = __dirname + '/__PAGE__'
export default async function Page() {
return (
<main>
<h1>Warmup Dev Renders</h1>
<CachedData label="page" cacheKey={CACHE_KEY} />
<SuccessiveCachedData label="page" cacheKey={CACHE_KEY} />
<CachedFetch label="page" cacheKey={CACHE_KEY} />
<Suspense fallback="Loading uncached fetch...">
<UncachedFetch label="page" cacheKey={CACHE_KEY} />
</Suspense>
</main>
)
}

View File

@@ -0,0 +1,44 @@
export default async function Page() {
return (
<main>
<h1>Warmup Dev Renders - deep successive cache reads</h1>
<One />
</main>
)
}
async function One() {
const cache1 = await fastCache()
console.log('after cache 1')
return <Two cache1={cache1} />
}
async function Two({ cache1 }) {
const cache2 = await slowCache(1)
console.log('after cache 2')
return <Three cache1={cache1} cache2={cache2} />
}
async function Three({ cache1, cache2 }) {
console.log('after caches 1 and 2')
const cache3 = await slowCache(2)
console.log('after cache 3')
return (
<div>
<div>Cache 1: {cache1}</div>
<div>Cache 2: {cache2}</div>
<div>Cache 3: {cache3}</div>
</div>
)
}
async function fastCache() {
'use cache'
return Math.random()
}
async function slowCache(_key: number) {
'use cache'
await new Promise((resolve) => setTimeout(resolve))
return Math.random()
}

View File

@@ -0,0 +1,29 @@
import { Suspense } from 'react'
import { CachedData, getCachedData } from '../../data-fetching'
import { cookies } from 'next/headers'
const CACHE_KEY = __dirname + '/__PAGE__'
export default async function Page() {
return (
<main>
<h1>Sync IO - runtime stage</h1>
<Suspense fallback={<div>Loading...</div>}>
<Runtime />
</Suspense>
</main>
)
}
async function Runtime() {
await getCachedData(CACHE_KEY + '-1')
console.log(`after first cache`)
await cookies()
console.log(`after cookies`)
Date.now()
console.log(`after sync io`)
return <CachedData label="page" cacheKey={CACHE_KEY} />
}

View File

@@ -0,0 +1,18 @@
import { CachedData, getCachedData } from '../../data-fetching'
const CACHE_KEY = __dirname + '/__PAGE__'
export default async function Page() {
await getCachedData(CACHE_KEY + '-1')
console.log(`after first cache`)
Date.now()
console.log(`after sync io`)
return (
<main>
<h1>Sync IO - static stage</h1>
<CachedData label="page" cacheKey={CACHE_KEY} />
</main>
)
}

View File

@@ -0,0 +1,7 @@
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig