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,25 @@
import { cookies } from 'next/headers'
import React from 'react'
async function getData() {
'use cache'
return fetch('https://next-data-api-endpoint.vercel.app/api/random', {
headers: {
Authorization: `Bearer ${process.env.MY_TOKEN}`,
},
}).then((res) => res.text())
}
export default async function Page() {
const myCookies = await cookies()
const id = myCookies.get('id')?.value
return (
<>
<p>index page</p>
<p id="random">{await getData()}</p>
<p id="my-id">{id || ''}</p>
</>
)
}

View File

@@ -0,0 +1,18 @@
async function getCached({ p }) {
'use cache'
const array = await p
if (array instanceof Uint8Array) {
return array[0].toString(16) + '-' + Math.random()
}
return 'invalid'
}
export default async function Page({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>
}) {
const n = parseInt((await searchParams).n as any, 16)
const p = Promise.resolve(new Uint8Array([n]))
return <p id="x">{await getCached({ p })}</p>
}

View File

@@ -0,0 +1,27 @@
async function getCachedRandom(x: number, children: React.ReactNode) {
'use cache: custom'
return {
x,
y: Math.random(),
r: children,
}
}
export default async function Page({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>
}) {
const n = +(await searchParams).n!
const values = await getCachedRandom(
n,
<p id="r">rnd{Math.random()}</p> // This should not invalidate the cache
)
return (
<>
<p id="x">{values.x}</p>
<p id="y">{values.y}</p>
{values.r}
</>
)
}

View File

@@ -0,0 +1,7 @@
import { setTimeout } from 'timers/promises'
export async function getCachedData() {
'use cache'
await setTimeout(1000)
return new Date().toISOString()
}

View File

@@ -0,0 +1,15 @@
import { getCachedData } from './cached-data'
export default async function Layout({
children,
}: {
children: React.ReactNode
}) {
return (
<>
<h1>Layout</h1>
<p id="layout-data">{await getCachedData()}</p>
<div>{children}</div>
</>
)
}

View File

@@ -0,0 +1,26 @@
import { Metadata } from 'next'
import { connection } from 'next/server'
import { getCachedData } from './cached-data'
export async function generateMetadata(): Promise<Metadata> {
// TODO: Deduping with nested caches requires #78703.
// 'use cache'
await connection()
return {
description: new Date().toISOString(),
title: await getCachedData(),
}
}
export default async function Page() {
await connection()
return (
<>
<h2>Page</h2>
<p id="page-data">{await getCachedData()}</p>
</>
)
}

View File

@@ -0,0 +1,34 @@
import { Viewport } from 'next'
export async function generateViewport({
params,
}: {
params: Promise<{ color: string }>
}): Promise<Viewport> {
'use cache: remote'
// Use `eval` to side step compiler errors for using `arguments` in a 'use
// cache' function.
// eslint-disable-next-line no-eval
const unusedParentArg = eval('arguments')[1]
if (unusedParentArg !== undefined) {
throw new Error(
'Expected the unused parent argument to be omitted. Received: ' +
unusedParentArg
)
}
// We're reading params here. This makes the cache function dynamic during
// prerendering. It also requires suspense above body, so nothing will
// prerendered. The meta tag should still be cached on refreshes though.
const { color } = await params
return {
colorScheme: color === 'white' ? 'light' : 'dark',
maximumScale: 1 + Math.random(),
}
}
export default function Layout({ children }: { children: React.ReactNode }) {
return <main>{children}</main>
}

View File

@@ -0,0 +1,43 @@
import { Viewport } from 'next'
import { connection } from 'next/server'
import { Suspense } from 'react'
export async function generateViewport({
params,
}: {
params: Promise<{ color: string }>
}): Promise<Viewport> {
'use cache: remote'
// Use `eval` to side step compiler errors for using `arguments` in a 'use
// cache' function.
// eslint-disable-next-line no-eval
const unusedParentArg = eval('arguments')[1]
if (unusedParentArg !== undefined) {
throw new Error(
'Expected the unused parent argument to be omitted. Received: ' +
unusedParentArg
)
}
// We're reading params here. This makes the cache function dynamic during
// prerendering. It also requires suspense above body, so nothing will
// prerendered. The meta tag should still be cached on refreshes though.
const { color } = await params
return { themeColor: color, initialScale: Math.random() }
}
export default function Page() {
return (
<Suspense fallback={<p>Loading...</p>}>
<Dynamic />
</Suspense>
)
}
async function Dynamic() {
await connection()
return <p>Dynamic</p>
}

View File

@@ -0,0 +1,18 @@
'use cache: remote'
import { withSlug } from './with-slug'
const Page = withSlug(function PageWithSlug({ slug }: { slug: string }) {
return (
<div>
<p>
Slug: <span id="slug">{slug}</span>
</p>
<p>
Date: <span id="date">{new Date().toISOString()}</span>
</p>
</div>
)
})
export default Page

View File

@@ -0,0 +1,12 @@
export function withSlug(
Component: React.ComponentType<{ slug: string }>
): React.ComponentType<{ params: Promise<{ slug: string }> }> {
return async function ComponentWithSlug(props: {
params: Promise<{ slug: string }>
}) {
const params = await props.params
const slug = params.slug
return <Component slug={slug} />
}
}

View File

@@ -0,0 +1,11 @@
import { Suspense } from 'react'
export default function Root({ children }: { children: React.ReactNode }) {
return (
<Suspense>
<html>
<body>{children}</body>
</html>
</Suspense>
)
}

View File

@@ -0,0 +1,31 @@
import { Foo } from '../client'
async function getCachedRandom(x: number, children: React.ReactNode) {
'use cache'
return {
x,
y: Math.random(),
z: <Foo />,
r: children,
}
}
export default async function Page({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>
}) {
const n = +(await searchParams).n!
const values = await getCachedRandom(
n,
<p id="r">rnd{Math.random()}</p> // This should not invalidate the cache
)
return (
<>
<p id="x">{values.x}</p>
<p id="y">{values.y}</p>
<p id="z">{values.z}</p>
{values.r}
</>
)
}

View File

@@ -0,0 +1,29 @@
import React from 'react'
import { cacheTag } from 'next/cache'
import Link from 'next/link'
import { connection } from 'next/server'
async function getCachedValue() {
'use cache'
cacheTag('revalidate-and-redirect')
return Math.random()
}
export default async function Page() {
// Make the page dynamic, as we don't want to deal with ISR in this scenario.
await connection()
const a = await getCachedValue()
const b = await getCachedValue()
return (
<div>
<p id="a">{a}</p>
<p id="b">{b}</p>
<Link href="/revalidate-and-redirect/redirect">
Go to /revalidate-and-redirect/redirect
</Link>
</div>
)
}

View File

@@ -0,0 +1,31 @@
import { revalidatePath, updateTag } from 'next/cache'
import { redirect } from 'next/navigation'
export default function Page() {
return (
<form>
<button
id="revalidate-tag-redirect"
formAction={async () => {
'use server'
updateTag('revalidate-and-redirect')
redirect('/revalidate-and-redirect')
}}
>
Revalidate tag and redirect
</button>{' '}
<button
id="revalidate-path-redirect"
formAction={async () => {
'use server'
revalidatePath('/revalidate-and-redirect')
redirect('/revalidate-and-redirect')
}}
>
Revalidate path and redirect
</button>
</form>
)
}

View File

@@ -0,0 +1,47 @@
'use client'
import { useActionState } from 'react'
export function Form({
revalidateAction,
initialValues,
}: {
revalidateAction: (type: 'tag' | 'path') => Promise<[number, number, string]>
initialValues: [number, number, string]
}) {
const [
[useCacheValue1, useCacheValue2, fetchedValue],
revalidate,
isPending,
] = useActionState(
async (_state: [number, number, string], type: 'tag' | 'path') =>
revalidateAction(type),
initialValues
)
return (
<form>
<p>
before revalidate: <span id="use-cache-value-1">{useCacheValue1}</span>
</p>
<p>
after revalidate: <span id="use-cache-value-2">{useCacheValue2}</span>
</p>
<p id="fetched-value">{fetchedValue}</p>
<button
id="revalidate-tag"
formAction={() => revalidate('tag')}
disabled={isPending}
>
Revalidate Tag
</button>
<button
id="revalidate-path"
formAction={() => revalidate('path')}
disabled={isPending}
>
Revalidate Path
</button>
</form>
)
}

View File

@@ -0,0 +1,47 @@
import { revalidatePath, cacheTag, updateTag } from 'next/cache'
import { Form } from './form'
import { connection } from 'next/server'
async function fetchCachedValue() {
return fetch('https://next-data-api-endpoint.vercel.app/api/random', {
next: { tags: ['revalidate-and-use'], revalidate: false },
}).then((res) => res.text())
}
async function getCachedValue() {
'use cache'
cacheTag('revalidate-and-use')
return Math.random()
}
export default async function Page() {
// Make the page dynamic, as we don't want to deal with ISR in this scenario.
await connection()
return (
<Form
revalidateAction={async (type: 'tag' | 'path') => {
'use server'
const initialCachedValue = await getCachedValue()
if (type === 'tag') {
updateTag('revalidate-and-use')
} else {
revalidatePath('/revalidate-and-use')
}
return Promise.all([
initialCachedValue,
getCachedValue(),
fetchCachedValue(),
])
}}
initialValues={await Promise.all([
getCachedValue(),
getCachedValue(),
fetchCachedValue(),
])}
/>
)
}

View File

@@ -0,0 +1,24 @@
import { connection } from 'next/server'
async function getRandomValue(offset: number) {
'use cache: remote'
return Math.random() + offset
}
let renderCount = 0
export default async function Page() {
await connection()
// Create the offsets array based on the render count to force a different
// array on each render.
const offsets = renderCount++ % 2 === 0 ? [0, 1] : [1, 0]
// Pass the function reference into the map function, which will pass the
// index and array as arguments into getRandomValue. This will create cache
// misses if the unused arguments are not properly ignored, because they would
// be included in the cache keys.
const randomNumbers = await Promise.all(offsets.map(getRandomValue))
return <p id="numbers">{randomNumbers.sort().join(' ')}</p>
}

View File

@@ -0,0 +1,14 @@
import { getCachedRandomWithCacheLife } from 'my-pkg'
export async function generateStaticParams() {
return [
{ id: `a${await getCachedRandomWithCacheLife(9)}` },
{ id: `b${await getCachedRandomWithCacheLife(2)}` },
]
}
export default async function Page() {
const value = getCachedRandomWithCacheLife(1)
return <p>{value}</p>
}

View File

@@ -0,0 +1,15 @@
import { getCachedRandomWithCacheLife } from 'my-pkg'
import { NextRequest } from 'next/server'
export async function generateStaticParams() {
return [{ id: await getCachedRandomWithCacheLife() }]
}
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
return Response.json({ id })
}

View File

@@ -0,0 +1,7 @@
import { revalidateTag } from 'next/cache'
import { redirect } from 'next/navigation'
export async function GET() {
revalidateTag('api', 'expireNow')
redirect('/api')
}

View File

@@ -0,0 +1,8 @@
import { getCachedRandomWithTag } from 'my-pkg'
export async function GET() {
const rand1 = await getCachedRandomWithTag('api')
const rand2 = await getCachedRandomWithTag('api')
return Response.json({ rand1, rand2 })
}

View File

@@ -0,0 +1,19 @@
import React from 'react'
async function getData() {
'use cache'
return fetch(
'https://next-data-api-endpoint.vercel.app/api/random?no-store',
{ cache: 'no-store' }
).then((res) => res.text())
}
export default async function Page() {
return (
<>
<p>index page</p>
<p id="random">{await getData()}</p>
</>
)
}

View File

@@ -0,0 +1,18 @@
import React from 'react'
async function getData() {
'use cache'
return fetch('https://next-data-api-endpoint.vercel.app/api/random').then(
(res) => res.text()
)
}
export default async function Page() {
return (
<>
<p>index page</p>
<p id="random">{await getData()}</p>
</>
)
}

View File

@@ -0,0 +1,36 @@
import { cacheLife } from 'next/cache'
import { connection } from 'next/server'
import { Suspense } from 'react'
async function getCachedRandom() {
'use cache'
cacheLife('frequent')
return Math.random()
}
async function DynamicCache() {
'use cache'
cacheLife({ revalidate: 99, expire: 299, stale: 18 })
return <p id="y">{new Date().toISOString()}</p>
}
async function Dynamic() {
await connection()
return null
}
export default async function Page() {
const x = await getCachedRandom()
return (
<>
<p id="x">{x}</p>
<Suspense fallback={<p id="y">Loading...</p>}>
<DynamicCache />
</Suspense>
<Suspense>
<Dynamic />
</Suspense>
</>
)
}

View File

@@ -0,0 +1,12 @@
import { cacheLife } from 'next/cache'
async function getCachedRandom() {
'use cache'
cacheLife('frequent')
return Math.random()
}
export default async function Page() {
const x = await getCachedRandom()
return <p id="x">{x}</p>
}

View File

@@ -0,0 +1,63 @@
import React from 'react'
import { revalidatePath, updateTag } from 'next/cache'
export function RevalidateButtons() {
return (
<form>
<button
id="revalidate-a"
formAction={async () => {
'use server'
updateTag('a')
}}
>
revalidate a
</button>{' '}
<button
id="revalidate-b"
formAction={async () => {
'use server'
updateTag('b')
}}
>
revalidate b
</button>{' '}
<button
id="revalidate-c"
formAction={async () => {
'use server'
updateTag('c')
}}
>
revalidate c
</button>{' '}
<button
id="revalidate-f"
formAction={async () => {
'use server'
updateTag('f')
}}
>
revalidate f
</button>{' '}
<button
id="revalidate-r"
formAction={async () => {
'use server'
updateTag('r')
}}
>
revalidate r
</button>{' '}
<button
id="revalidate-path"
formAction={async () => {
'use server'
revalidatePath('/cache-tag')
}}
>
revalidate path
</button>
</form>
)
}

View File

@@ -0,0 +1,58 @@
import React from 'react'
import { cacheTag } from 'next/cache'
import { RevalidateButtons } from './buttons'
async function getCachedWithTag({
tag,
fetchCache,
}: {
tag: string
fetchCache?: 'force' | 'revalidate'
}) {
'use cache'
cacheTag(tag, 'c')
// If `force-cache` or `revalidate` is used for the fetch call, it creates
// basically an inner cache, and revalidating tag 'c' won't revalidate the
// fetch cache. If both are not used, the fetch is not cached at all in the
// fetch cache, and is included in the cached result of `getCachedWithTag`
// instead, thus also affected by revalidating 'c'.
const response = await fetch(
`https://next-data-api-endpoint.vercel.app/api/random?tag=${tag}`,
{
cache: fetchCache === 'force' ? 'force-cache' : undefined,
next: { revalidate: fetchCache === 'revalidate' ? 42 : undefined },
}
)
const fetchedValue = await response.text()
return [Math.random(), fetchedValue]
}
export default async function Page() {
const a = await getCachedWithTag({ tag: 'a' })
const b = await getCachedWithTag({ tag: 'b' })
const [f1, f2] = await getCachedWithTag({
tag: 'f',
fetchCache: 'force',
})
const [r1, r2] = await getCachedWithTag({
tag: 'r',
fetchCache: 'revalidate',
})
return (
<div>
<p id="a">[a, c] {a.join(' ')}</p>
<p id="b">[b, c] {b.join(' ')}</p>
<p id="f1">[f, c] {f1}</p>
<p id="f2">[-] {f2}</p>
<p id="r1">[r, c] {r1}</p>
<p id="r2">[-] {r2}</p>
<RevalidateButtons />
</div>
)
}

View File

@@ -0,0 +1,17 @@
import { getCachedRandomWithHandler } from 'my-pkg'
export default async function Page() {
const one = await getCachedRandomWithHandler()
const two = await getCachedRandomWithHandler()
return (
<main>
<div>
One: <span id="one">{one}</span>
</div>
<div>
Two: <span id="two">{two}</span>
</div>
{one === two ? '✅ Cached' : '❌ Not cached'}
</main>
)
}

View File

@@ -0,0 +1,17 @@
import { getCachedRandom } from 'my-pkg'
export default async function Page() {
const one = await getCachedRandom()
const two = await getCachedRandom()
return (
<main>
<div>
One: <span id="one">{one}</span>
</div>
<div>
Two: <span id="two">{two}</span>
</div>
{one === two ? '✅ Cached' : '❌ Not cached'}
</main>
)
}

View File

@@ -0,0 +1,17 @@
'use client'
import React from 'react'
import { useFormStatus } from 'react-dom'
export function Button({
children,
id,
}: React.PropsWithChildren<{ id: string }>) {
const { pending } = useFormStatus()
return (
<button id={id} disabled={pending}>
{children}
</button>
)
}

View File

@@ -0,0 +1,81 @@
import { cookies, draftMode } from 'next/headers'
import { Button } from './button'
async function getCachedValue(
iterable: Iterable<number>,
fn: () => string
): Promise<[string, () => string]> {
'use cache'
// Make sure we're always receiving the arguments the same way, regardless of
// whether draft mode is enabled or not. We're asserting that by checking
// whether an iterable was serialized/deserialized into an array by React.
if (!Array.isArray(iterable)) {
throw new Error(
'Expected iterable to be serialized to an array because it crossed the "use cache" boundary.'
)
}
const date = new Date()
date.setFullYear(date.getFullYear() + iterable.reduce((sum, n) => sum + n))
return [date.toISOString(), fn]
}
export default async function Page({
params,
}: {
params: Promise<{ mode: string }>
}) {
'use cache'
const { mode } = await params
const offset = 1000
const cachedClosure = async () => {
'use cache'
return new Date(Date.now() + offset).toISOString()
}
const { isEnabled } = await draftMode()
// Accessing request-scoped data in "use cache" should not be allowed, even if
// draft mode is enabled. We expect the access to throw.
if (isEnabled && mode === 'with-cookies') {
await cookies()
}
const [cachedValue, passthroughFn] = await getCachedValue(
{
[Symbol.iterator]: function* () {
yield 1
yield 2
yield 3
},
},
() => 'value from passed-through function'
)
return (
<form
action={async () => {
'use server'
const draft = await draftMode()
if (draft.isEnabled) {
draft.disable()
} else {
draft.enable()
}
}}
>
<p id="top-level">{cachedValue}</p>
<p id="closure">{await cachedClosure()}</p>
<p>{passthroughFn()}</p>
<Button id="toggle">{isEnabled ? 'Disable' : 'Enable'} Draft Mode</Button>
</form>
)
}
export function generateStaticParams() {
return [{ mode: 'with-cookies' }, { mode: 'without-cookies' }]
}

View File

@@ -0,0 +1,18 @@
import React from 'react'
async function getData() {
'use cache'
return fetch('https://next-data-api-endpoint.vercel.app/api/random', {
next: { revalidate: 1 },
}).then((res) => res.text())
}
export default async function Page() {
return (
<>
<p>index page</p>
<p id="random">{await getData()}</p>
</>
)
}

View File

@@ -0,0 +1,18 @@
import { updateTag, cacheTag } from 'next/cache'
async function refresh() {
'use server'
updateTag('home')
}
export default async function Page() {
'use cache'
cacheTag('home')
return (
<form action={refresh}>
<button id="refresh">Refresh</button>
<p id="t">{new Date().toISOString()}</p>
</form>
)
}

View File

@@ -0,0 +1,24 @@
import { Metadata, ResolvingMetadata } from 'next'
export async function generateMetadata(
_: { params: Promise<{ slug: string }> },
parent: ResolvingMetadata
): Promise<Metadata> {
'use cache'
// We're not reading params here, but we do define a canonical URL, which
// leads to the pathname being read under the hood. This should make the
// function dynamic when prerendering the fallback shell, and not lead to a
// timeout error.
const { metadataBase } = await parent
return {
// We can not return a URL instance from a `'use cache'` function.
metadataBase: metadataBase?.replace('/foo', '/bar'),
}
}
export default function Layout({ children }: { children: React.ReactNode }) {
return <>{children}</>
}

View File

@@ -0,0 +1,36 @@
import { Metadata, ResolvingMetadata } from 'next'
import { connection } from 'next/server'
import { Suspense } from 'react'
export async function generateMetadata(
_: { params: Promise<{ slug: string }> },
parent: ResolvingMetadata
): Promise<Metadata> {
'use cache'
// We're not reading params here, but we do define a canonical URL, which
// leads to the pathname being read under the hood. This should make the
// function dynamic when prerendering the fallback shell, and not lead to a
// timeout error.
const { metadataBase } = await parent
return {
metadataBase: metadataBase?.replace('/bar', '/baz'),
alternates: { canonical: '/qux' },
}
}
export default function Page() {
return (
<Suspense fallback={<p>Loading...</p>}>
<Dynamic />
</Suspense>
)
}
async function Dynamic() {
await connection()
return <p>Dynamic</p>
}

View File

@@ -0,0 +1,9 @@
import { Metadata } from 'next'
export const metadata: Metadata = {
metadataBase: new URL('https://example.com/foo'),
}
export default function Layout({ children }: { children: React.ReactNode }) {
return <main>{children}</main>
}

View File

@@ -0,0 +1,20 @@
import { Metadata, ResolvingMetadata } from 'next'
export async function generateMetadata(
_: {},
parent: ResolvingMetadata
): Promise<Metadata> {
'use cache'
const { metadataBase } = await parent
return {
description: new Date().toISOString(),
// We can not return a URL instance from a `'use cache'` function.
metadataBase: metadataBase?.replace('/foo', '/bar'),
}
}
export default function Layout({ children }: { children: React.ReactNode }) {
return <>{children}</>
}

View File

@@ -0,0 +1,36 @@
import { Metadata, ResolvingMetadata } from 'next'
import { connection } from 'next/server'
import { Suspense } from 'react'
export async function generateMetadata(
_: { searchParams: Promise<Record<string, string | string[] | undefined>> },
parent: ResolvingMetadata
): Promise<Metadata> {
'use cache'
// Explicitly not reading search params here. The search params should be
// omitted from the cache key, so that we ensure a cache hit when resuming the
// partially prerendered page.
const { metadataBase } = await parent
return {
title: new Date().toISOString(),
metadataBase: metadataBase?.replace('/bar', '/baz'),
alternates: { canonical: '/qux' },
}
}
export default function Page() {
return (
<Suspense fallback={<p>Loading...</p>}>
<Dynamic />
</Suspense>
)
}
async function Dynamic() {
await connection()
return <p>Dynamic</p>
}

View File

@@ -0,0 +1,22 @@
import { Metadata } from 'next'
export async function generateMetadata(_: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
'use cache'
// Explicitly not reading params here. The description should appear in the
// partially prerendered page. TODO: When resuming the page, we should get a
// cache hit (from the RDC), but omitting unused params from cache keys (and
// upgrading cache keys when they are used) is not yet implemented.
// Make sure this cache doesn't resolve instantly,
// so that if it causes a cache miss, it's noticeable.
await new Promise((resolve) => setTimeout(resolve, 5))
return { description: new Date().toISOString() }
}
export default function Layout({ children }: { children: React.ReactNode }) {
return <>{children}</>
}

View File

@@ -0,0 +1,34 @@
import { Metadata } from 'next'
import { connection } from 'next/server'
import { Suspense } from 'react'
export async function generateMetadata(_: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
'use cache'
// Explicitly not reading params here. The title should appear in the
// partially prerendered page. TODO: When resuming the page, we should get a
// cache hit (from the RDC), but omitting unused params from cache keys (and
// upgrading cache keys when they are used) is not yet implemented.
// Make sure this cache doesn't resolve instantly,
// so that if it causes a cache miss, it's noticeable.
await new Promise((resolve) => setTimeout(resolve, 5))
return { title: new Date().toISOString() }
}
export default function Page() {
return (
<Suspense fallback={<p>Loading...</p>}>
<Dynamic />
</Suspense>
)
}
async function Dynamic() {
await connection()
return <p>Dynamic</p>
}

View File

@@ -0,0 +1,31 @@
import { Metadata } from 'next'
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
'use cache: remote'
// Use `eval` to side step compiler errors for using `arguments` in a 'use
// cache' function.
// eslint-disable-next-line no-eval
const unusedParentArg = eval('arguments')[1]
if (unusedParentArg !== undefined) {
throw new Error(
'Expected the unused parent argument to be omitted. Received: ' +
unusedParentArg
)
}
// We're reading params here. This makes the cache function dynamic during
// prerendering, and thus the description should be excluded from the
// partially prerendered page.
const { slug } = await params
return { description: new Date().toISOString(), category: slug }
}
export default function Layout({ children }: { children: React.ReactNode }) {
return <main>{children}</main>
}

View File

@@ -0,0 +1,43 @@
import { Metadata } from 'next'
import { connection } from 'next/server'
import { Suspense } from 'react'
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
'use cache: remote'
// Use `eval` to side step compiler errors for using `arguments` in a 'use
// cache' function.
// eslint-disable-next-line no-eval
const unusedParentArg = eval('arguments')[1]
if (unusedParentArg !== undefined) {
throw new Error(
'Expected the unused parent argument to be omitted. Received: ' +
unusedParentArg
)
}
// We're reading params here. This makes the cache function dynamic during
// prerendering, and thus the title should be excluded from the partially
// prerendered page.
const { slug } = await params
return { title: new Date().toISOString(), keywords: [slug] }
}
export default function Page() {
return (
<Suspense fallback={<p>Loading...</p>}>
<Dynamic />
</Suspense>
)
}
async function Dynamic() {
await connection()
return <p>Dynamic</p>
}

View File

@@ -0,0 +1,31 @@
import { Viewport } from 'next'
import { connection } from 'next/server'
import { Suspense } from 'react'
export async function generateViewport({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>
}): Promise<Viewport> {
'use cache'
// Explicitly not reading search params here. The search params should be
// omitted from the cache key, so that we ensure a cache hit when resuming the
// partially prerendered page.
return { initialScale: Math.random() }
}
export default function Page() {
return (
<Suspense fallback={<p>Loading...</p>}>
<Dynamic />
</Suspense>
)
}
async function Dynamic() {
await connection()
return <p>Dynamic</p>
}

View File

@@ -0,0 +1,20 @@
import { Viewport } from 'next'
export async function generateViewport({
params,
}: {
params: Promise<{ color: string }>
}): Promise<Viewport> {
'use cache'
// Explicitly not reading params here. The meta tag should appear in the
// partially prerendered page. TODO: When resuming the page, we should get a
// cache hit (from the RDC), but omitting unused params from cache keys (and
// upgrading cache keys when they are used) is not yet implemented.
return { maximumScale: 1 + Math.random() }
}
export default function Layout({ children }: { children: React.ReactNode }) {
return <main>{children}</main>
}

View File

@@ -0,0 +1,32 @@
import { Viewport } from 'next'
import { connection } from 'next/server'
import { Suspense } from 'react'
export async function generateViewport({
params,
}: {
params: Promise<{ color: string }>
}): Promise<Viewport> {
'use cache'
// Explicitly not reading params here. The meta tag should appear in the
// partially prerendered page. TODO: When resuming the page, we should get a
// cache hit (from the RDC), but omitting unused params from cache keys (and
// upgrading cache keys when they are used) is not yet implemented.
return { initialScale: Math.random() }
}
export default function Page() {
return (
<Suspense fallback={<p>Loading...</p>}>
<Dynamic />
</Suspense>
)
}
async function Dynamic() {
await connection()
return <p>Dynamic</p>
}

View File

@@ -0,0 +1,25 @@
'use cache'
import { cacheLife, cacheTag } from 'next/cache'
function getRandomValue() {
const v = Math.random()
console.log(v)
return v
}
export async function foo() {
cacheLife('days')
return getRandomValue()
}
export const bar = async function () {
cacheTag('bar')
return getRandomValue()
}
const baz = async () => {
return getRandomValue()
}
export { baz }

View File

@@ -0,0 +1,8 @@
'use client'
import { foo, bar, baz } from './cached'
import { Form } from '../../form'
export function ClientComponent() {
return <Form foo={foo} bar={bar} baz={baz} />
}

View File

@@ -0,0 +1,5 @@
import { ClientComponent } from './client-component'
export default function Page() {
return <ClientComponent />
}

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,18 @@
async function Bar() {
'use cache'
const date = new Date().toLocaleTimeString()
console.log('deep inside', date)
return <p>{date}</p>
}
async function Foo() {
'use cache'
console.log('inside')
return <Bar />
}
export default async function Page() {
console.log('outside')
return <Foo />
}

View File

@@ -0,0 +1,10 @@
export function createCached(n: number) {
return {
async getRandomValue() {
'use cache'
const v = n + Math.random()
console.log(v)
return v
},
}
}

View File

@@ -0,0 +1,20 @@
'use client'
import { useActionState } from 'react'
export function Form({
id,
getRandomValue,
}: {
id: string
getRandomValue: () => Promise<number>
}) {
const [result, formAction, isPending] = useActionState(getRandomValue, -1)
return (
<form id={id} action={formAction}>
<button>Submit</button>
<p>{isPending ? 'loading...' : result}</p>
</form>
)
}

View File

@@ -0,0 +1,14 @@
import { createCached } from './cached'
import { Form } from './form'
export default function Page() {
const cached1 = createCached(1)
const cached2 = createCached(2)
return (
<main>
<Form id="form-1" getRandomValue={cached1.getRandomValue} />
<Form id="form-2" getRandomValue={cached2.getRandomValue} />
</main>
)
}

View File

@@ -0,0 +1,24 @@
import { revalidatePath, unstable_cache } from 'next/cache'
async function inner() {
'use cache'
return Math.random()
}
const outer = unstable_cache(async () => {
return inner()
})
export default async function Page() {
return (
<form
action={async () => {
'use server'
revalidatePath('/nested-in-unstable-cache')
}}
>
<p>{await outer()}</p>
<button>Revalidate path</button>
</form>
)
}

View File

@@ -0,0 +1,9 @@
'use cache'
import { notFound } from 'next/navigation'
export default async function Page() {
notFound()
return <p>This will never render</p>
}

View File

@@ -0,0 +1,22 @@
import { revalidatePath } from 'next/cache'
import { RevalidateButtons } from './revalidate-buttons'
async function cachedValue() {
'use cache'
return Math.random()
}
export default async function Page() {
const value = await cachedValue()
return (
<div>
<p id="value">{value}</p>
<RevalidateButtons
revalidatePath={async () => {
'use server'
revalidatePath('/on-demand-revalidate')
}}
/>
</div>
)
}

View File

@@ -0,0 +1,32 @@
'use client'
import { useTransition } from 'react'
export function RevalidateButtons({
revalidatePath,
}: {
revalidatePath: () => Promise<void>
}) {
const [isPending, startTransition] = useTransition()
return (
<form>
<button id="revalidate-path" formAction={revalidatePath}>
revalidate with revalidatePath()
</button>{' '}
<button
id="revalidate-api-route"
disabled={isPending}
formAction={async () => {
startTransition(async () => {
await fetch('/api/revalidate?path=/on-demand-revalidate', {
method: 'POST',
})
})
}}
>
revalidate with API route
</button>
</form>
)
}

View File

@@ -0,0 +1,27 @@
import { Form } from '../../form'
function getRandomValue() {
const v = Math.random()
console.log(v)
return v
}
export default function Page() {
const offset = 100
return (
<Form
foo={async function fooNamed() {
'use cache'
return offset + getRandomValue()
}}
bar={async function () {
'use cache'
return offset + getRandomValue()
}}
baz={async () => {
'use cache'
return offset + getRandomValue()
}}
/>
)
}

View File

@@ -0,0 +1,51 @@
import { cacheLife } from 'next/cache'
import { connection } from 'next/server'
import { Suspense } from 'react'
async function outermost(id: string) {
'use cache'
return id + middle('middle')
}
async function middle(id: string) {
'use cache'
return id + innermost('inner')
}
async function innermost(id: string) {
'use cache'
return id
}
async function Short({ id }: { id: string }) {
'use cache'
cacheLife('seconds')
return id
}
async function Dynamic() {
await connection()
return null
}
async function CachedStuff() {
await outermost('outer')
await innermost('inner')
return (
<Suspense>
<Short id="short" />
</Suspense>
)
}
export default function Page() {
return (
<div>
<CachedStuff />
<Suspense>
<Dynamic />
</Suspense>
</div>
)
}

View File

@@ -0,0 +1,24 @@
import { cache } from 'react'
const number = cache(() => {
return Math.random()
})
function Component() {
// Read number again in a component. This should be deduped.
return <p id="b">{number()}</p>
}
async function getCachedComponent() {
'use cache'
return (
<div>
<p id="a">{number()}</p>
<Component />
</div>
)
}
export default async function Page() {
return <div>{getCachedComponent()}</div>
}

View File

@@ -0,0 +1,61 @@
/* eslint-disable no-self-compare */
async function getObject(arg: unknown) {
'use cache'
return { arg }
}
async function getObjectWithBoundArgs(arg: unknown) {
async function getCachedObject() {
'use cache'
return { arg }
}
return getCachedObject()
}
export default async function Page() {
return (
<>
<h2>With normal args</h2>
<p>
Two <span style={{ whiteSpace: 'pre' }}>"use cache"</span> invocations
with the same arg return the same result:{' '}
<strong id="same-arg">
{String((await getObject(1)) === (await getObject(1)))}
</strong>
</p>
<p>
Two <span style={{ whiteSpace: 'pre' }}>"use cache"</span> invocations
with different args return different results:{' '}
<strong id="different-args">
{String((await getObject(1)) !== (await getObject(2)))}
</strong>
</p>
<h2>With bound args</h2>
<p>
Two <span style={{ whiteSpace: 'pre' }}>"use cache"</span> invocations
with the same bound arg return the same result:{' '}
<strong id="same-bound-arg">
{String(
(await getObjectWithBoundArgs(1)) ===
(await getObjectWithBoundArgs(1))
)}
</strong>
</p>
<p>
Two <span style={{ whiteSpace: 'pre' }}>"use cache"</span> invocations
with different bound args return different results:{' '}
<strong id="different-bound-args">
{String(
(await getObjectWithBoundArgs(1)) !==
(await getObjectWithBoundArgs(2))
)}
</strong>
</p>
</>
)
}

View File

@@ -0,0 +1,34 @@
import { revalidateTag, cacheTag, cacheLife } from 'next/cache'
async function getCachedRandomNumber() {
'use cache'
cacheTag('revalidate-tag-test')
cacheLife('max')
// This should change on each cache refresh
return Math.random().toString()
}
export default async function Page() {
const randomNumber = await getCachedRandomNumber()
return (
<div>
<p id="random">{randomNumber}</p>
<form>
<button
id="revalidate-tag-with-profile"
formAction={async () => {
'use server'
// This should NOT cause immediate client refresh
// The client should continue showing stale data
// Fresh data should only appear on next navigation/refresh
revalidateTag('revalidate-tag-test', 'max')
}}
>
Revalidate Tag (background)
</button>
</form>
</div>
)
}

View File

@@ -0,0 +1,7 @@
'use client'
export function ClientComponent({ children, data }) {
console.log(data)
return children
}

View File

@@ -0,0 +1,20 @@
import { ClientComponent } from './client'
async function getLargeObject() {
'use cache'
return { hello: 'world' }
}
export default async function Page() {
return (
<>
<ClientComponent data={await getLargeObject()}>
<p>foo</p>
</ClientComponent>
<ClientComponent data={await getLargeObject()}>
<p>bar</p>
</ClientComponent>
</>
)
}

View File

@@ -0,0 +1,95 @@
import { connection } from 'next/server'
import { cacheLife } from 'next/cache'
import { Suspense } from 'react'
async function revalidateZero() {
'use cache: remote'
cacheLife({ revalidate: 0 })
return new Date().toISOString()
}
async function lowExpire() {
'use cache: remote'
cacheLife({ expire: 5 })
return new Date().toISOString()
}
async function OuterCacheNoExplicit() {
'use cache: remote'
// No explicit cacheLife - this would error during prerendering, but is
// allowed at request time (after connection()).
return (
<>
<p>
<code>revalidate=0</code>:{' '}
<span id="revalidate-zero">{await revalidateZero()}</span>
</p>
<p>
<code>expire=5</code>: <span id="low-expire">{await lowExpire()}</span>
</p>
</>
)
}
async function OuterCacheExplicitShort() {
'use cache: remote'
// Explicit short cacheLife - excluded from prerender, becomes a dynamic hole.
cacheLife({ revalidate: 0, expire: 5 })
return (
<>
<p>
Explicit <code>revalidate=0</code>:{' '}
<span id="explicit-revalidate-zero">{await revalidateZero()}</span>
</p>
<p>
Explicit <code>expire=5</code>:{' '}
<span id="explicit-low-expire">{await lowExpire()}</span>
</p>
</>
)
}
async function OuterCacheExplicitLong() {
'use cache: remote'
// Explicit long cacheLife - included in prerender despite short-lived inner
// caches.
cacheLife('default')
return (
<>
<p>
Explicit long (<code>revalidate=0</code> inner):{' '}
<span id="explicit-long-revalidate-zero">{await revalidateZero()}</span>
</p>
<p>
Explicit long (<code>expire=5</code> inner):{' '}
<span id="explicit-long-low-expire">{await lowExpire()}</span>
</p>
</>
)
}
async function Dynamic() {
await connection()
return <OuterCacheNoExplicit />
}
export default async function Page() {
return (
<>
<p id="static">Static content</p>
<Suspense fallback={<p id="dynamic">Loading...</p>}>
<Dynamic />
</Suspense>
<Suspense fallback={<p>Loading explicit short...</p>}>
<OuterCacheExplicitShort />
</Suspense>
<OuterCacheExplicitLong />
</>
)
}

View File

@@ -0,0 +1,8 @@
export class Cached {
static async getRandomValue() {
'use cache'
const v = Math.random()
console.log(v)
return v
}
}

View File

@@ -0,0 +1,18 @@
'use client'
import { useActionState } from 'react'
export function Form({
getRandomValue,
}: {
getRandomValue: () => Promise<number>
}) {
const [result, formAction, isPending] = useActionState(getRandomValue, -1)
return (
<form action={formAction}>
<button>Submit</button>
<p>{isPending ? 'loading...' : result}</p>
</form>
)
}

View File

@@ -0,0 +1,6 @@
import { Cached } from './cached'
import { Form } from './form'
export default function Page() {
return <Form getRandomValue={Cached.getRandomValue} />
}

View File

@@ -0,0 +1,33 @@
import { Suspense } from 'react'
import { cookies, headers } from 'next/headers'
import { connection } from 'next/server'
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
{children}
<Suspense>
<HangingCalls />
</Suspense>
</body>
</html>
)
}
async function HangingCalls() {
// We purposely do not await these to prevent React from suppressing the rejection error
cookies().then(() => {})
headers().then(() => {})
connection().then(() => {})
return null
}

View File

@@ -0,0 +1,3 @@
export default function Loading() {
return 'loading...'
}

View File

@@ -0,0 +1,15 @@
export default async function Page() {
console.log('rendering Page')
return (
<div>
This test doesn't actually render any caches. It was added because when
"use cache" was first introduced without cacheComponents there was a bug
where we still performed a dev only warmup render which has
cacheComponents semantics which led to the observation of hanging promise
rejections in the CLI. `cookies()` and related Request data APIs should
never be hanging promises when `cacheComponents` is disabled and this was
corrected but this test helps ensure that we don't regress in this manner
later
</div>
)
}

View File

@@ -0,0 +1,7 @@
'use cache'
const getRandomValue = async () => {
return Math.random()
}
export { getRandomValue }

View File

@@ -0,0 +1,15 @@
'use client'
import { useActionState } from 'react'
import { getRandomValue } from './cached'
export function Form() {
const [result, formAction, isPending] = useActionState(getRandomValue, -1)
return (
<form action={formAction}>
<button id="submit-button">Submit</button>
<p>{isPending ? 'loading...' : result}</p>
</form>
)
}

View File

@@ -0,0 +1,5 @@
import { Form } from './form'
export default function Page() {
return <Form />
}

View File

@@ -0,0 +1,5 @@
'use cache'
export async function getRandomValue() {
return Math.random()
}

View File

@@ -0,0 +1,15 @@
'use client'
import { useActionState } from 'react'
import { getRandomValue } from './cached'
export function Form() {
const [result, formAction, isPending] = useActionState(getRandomValue, -1)
return (
<form action={formAction}>
<button id="submit-button">Submit</button>
<p>{isPending ? 'loading...' : result}</p>
</form>
)
}

View File

@@ -0,0 +1,5 @@
import { Form } from './form'
export default function Page() {
return <Form />
}

View File

@@ -0,0 +1,22 @@
'use client'
import { ReactNode } from 'react'
import { useActionState } from 'react'
export function Form({
action,
children,
}: {
action: () => Promise<string>
children: ReactNode
}) {
const [result, formAction] = useActionState(action, 'initial')
return (
<form action={formAction}>
<button>Submit</button>
<p>{result}</p>
{children}
</form>
)
}

View File

@@ -0,0 +1,17 @@
import { Form } from './form'
async function action() {
'use server'
return 'result'
}
export default async function Page() {
'use cache'
return (
<Form action={action}>
<p>{Date.now()}</p>
</Form>
)
}

View File

@@ -0,0 +1,5 @@
'use client'
export function Foo() {
return 'foo'
}

View File

@@ -0,0 +1,39 @@
'use client'
import { useActionState } from 'react'
export function Form({
foo,
bar,
baz,
}: {
foo: () => Promise<number>
bar: () => Promise<number>
baz: () => Promise<number>
}) {
const [result, dispatch] = useActionState<
[number, number, number],
'submit' | 'reset'
>(
async (_state, event) => {
if (event === 'reset') {
return [0, 0, 0]
}
return [await foo(), await bar(), await baz()]
},
[0, 0, 0]
)
return (
<form action={() => dispatch('submit')}>
<button id="submit-button">Submit</button>{' '}
<button id="reset-button" formAction={() => dispatch('reset')}>
Reset
</button>
<p>
{result[0]} {result[1]} {result[2]}
</p>
</form>
)
}

View File

@@ -0,0 +1,13 @@
const {
default: FileSystemCache,
} = require('next/dist/server/lib/incremental-cache/file-system-cache')
module.exports = class IncrementalCacheHandler extends FileSystemCache {
async set(key, data, ctx) {
if (ctx.fetchCache) {
console.log('cache-handler set fetch cache', ctx.fetchUrl)
}
return super.set(key, data, ctx)
}
}

View File

@@ -0,0 +1,28 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
cacheHandlers: {
custom: require.resolve(
'next/dist/server/lib/cache-handlers/default.external'
),
},
experimental: {
useCache: true,
},
cacheLife: {
frequent: {
stale: 19,
revalidate: 100,
expire: 300,
},
expireNow: {
stale: 0,
expire: 0,
revalidate: 0,
},
},
cacheHandler: require.resolve('./incremental-cache-handler'),
}
module.exports = nextConfig

View File

@@ -0,0 +1,23 @@
import { cacheLife, cacheTag } from 'next/cache'
export async function getCachedRandom() {
'use cache'
return Math.random()
}
export async function getCachedRandomWithTag(tag) {
'use cache'
cacheTag(tag)
return Math.random()
}
export async function getCachedRandomWithCacheLife(n = 1) {
'use cache'
cacheLife('weeks')
return String(Math.ceil(Math.random() * n))
}
export async function getCachedRandomWithHandler() {
'use cache: default'
return Math.random()
}

View File

@@ -0,0 +1,7 @@
{
"name": "my-pkg",
"type": "module",
"exports": {
".": "./index.js"
}
}

View File

@@ -0,0 +1,22 @@
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const pathParam = req.query['path']
if (!pathParam) {
return res.status(400).send(`Missing required query param "path"`)
}
const paths = Array.isArray(pathParam) ? pathParam : [pathParam]
try {
await Promise.all(paths.map((path) => res.revalidate(path)))
return res.json({ revalidated: true })
} catch (err) {
console.error(err)
return res.status(500).send(`Error revalidating ${paths}`)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because it is too large Load Diff