From 68bc669e5640015006a36fe8d773963938f718b4 Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Mon, 28 Jul 2025 22:16:55 +0200 Subject: [PATCH] allow root params access in private caches --- .../work-unit-async-storage.external.ts | 7 ++++ .../next/src/server/request/root-params.ts | 4 +- .../src/server/use-cache/use-cache-wrapper.ts | 15 ++++---- .../app/[lang]/[locale]/layout.tsx | 16 ++++++++ .../[locale]/use-cache-private/page.tsx | 34 +++++++++++++++++ .../fixtures/use-cache-private/next.config.ts | 10 +++++ .../app-root-params-getters/use-cache.test.ts | 38 ++++++++++++------- 7 files changed, 101 insertions(+), 23 deletions(-) create mode 100644 test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/app/[lang]/[locale]/layout.tsx create mode 100644 test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/app/[lang]/[locale]/use-cache-private/page.tsx create mode 100644 test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/next.config.ts diff --git a/packages/next/src/server/app-render/work-unit-async-storage.external.ts b/packages/next/src/server/app-render/work-unit-async-storage.external.ts index 321750f5dd29..878f440af2e3 100644 --- a/packages/next/src/server/app-render/work-unit-async-storage.external.ts +++ b/packages/next/src/server/app-render/work-unit-async-storage.external.ts @@ -246,6 +246,13 @@ export interface PrivateUseCacheStore extends CommonUseCacheStore { * access the request cookies. */ readonly cookies: ReadonlyRequestCookies + + /** + * Private caches don't currently need to track root params in the cache key + * because they're not persisted anywhere, so we can allow root params access + * (unlike public caches) + */ + readonly rootParams: Params } export type UseCacheStore = PublicUseCacheStore | PrivateUseCacheStore diff --git a/packages/next/src/server/request/root-params.ts b/packages/next/src/server/request/root-params.ts index 207f299b5485..e3fb74a4f72a 100644 --- a/packages/next/src/server/request/root-params.ts +++ b/packages/next/src/server/request/root-params.ts @@ -41,7 +41,6 @@ export async function unstable_rootParams(): Promise { switch (workUnitStore.type) { case 'cache': - case 'private-cache': case 'unstable-cache': { throw new Error( `Route ${workStore.route} used \`unstable_rootParams()\` inside \`"use cache"\` or \`unstable_cache\`. Support for this API inside cache scopes is planned for a future version of Next.js.` @@ -56,6 +55,7 @@ export async function unstable_rootParams(): Promise { workStore, workUnitStore ) + case 'private-cache': case 'request': return Promise.resolve(workUnitStore.rootParams) default: @@ -228,7 +228,6 @@ export function getRootParam(paramName: string): Promise { switch (workUnitStore.type) { case 'unstable-cache': - case 'private-cache': case 'cache': { throw new Error( `Route ${workStore.route} used ${apiName} inside \`"use cache"\` or \`unstable_cache\`. Support for this API inside cache scopes is planned for a future version of Next.js.` @@ -245,6 +244,7 @@ export function getRootParam(paramName: string): Promise { apiName ) } + case 'private-cache': case 'request': { break } diff --git a/packages/next/src/server/use-cache/use-cache-wrapper.ts b/packages/next/src/server/use-cache/use-cache-wrapper.ts index 50a57f745917..8c28b6de2f4e 100644 --- a/packages/next/src/server/use-cache/use-cache-wrapper.ts +++ b/packages/next/src/server/use-cache/use-cache-wrapper.ts @@ -187,14 +187,15 @@ function createUseCacheStore( explicitExpire: undefined, explicitStale: undefined, tags: null, - hmrRefreshHash: - outerWorkUnitStore && getHmrRefreshHash(workStore, outerWorkUnitStore), - isHmrRefresh: outerWorkUnitStore?.isHmrRefresh ?? false, - serverComponentsHmrCache: outerWorkUnitStore?.serverComponentsHmrCache, + hmrRefreshHash: getHmrRefreshHash(workStore, outerWorkUnitStore), + isHmrRefresh: outerWorkUnitStore.isHmrRefresh ?? false, + serverComponentsHmrCache: outerWorkUnitStore.serverComponentsHmrCache, forceRevalidate: shouldForceRevalidate(workStore, outerWorkUnitStore), - draftMode: - outerWorkUnitStore && - getDraftModeProviderForCacheScope(workStore, outerWorkUnitStore), + draftMode: getDraftModeProviderForCacheScope( + workStore, + outerWorkUnitStore + ), + rootParams: outerWorkUnitStore.rootParams, cookies: outerWorkUnitStore.cookies, } } else { diff --git a/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/app/[lang]/[locale]/layout.tsx b/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/app/[lang]/[locale]/layout.tsx new file mode 100644 index 000000000000..3a3c2e2a67e2 --- /dev/null +++ b/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/app/[lang]/[locale]/layout.tsx @@ -0,0 +1,16 @@ +import { ReactNode } from 'react' + +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} + +export function generateStaticParams() { + // the param values are not accessed in tests, + // we just need a value here to avoid errors in PPR/cacheComponents + // where we need to provide at least one set of values for root params + return [{ lang: 'foo', locale: 'bar' }] +} diff --git a/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/app/[lang]/[locale]/use-cache-private/page.tsx b/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/app/[lang]/[locale]/use-cache-private/page.tsx new file mode 100644 index 000000000000..c8107df922c8 --- /dev/null +++ b/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/app/[lang]/[locale]/use-cache-private/page.tsx @@ -0,0 +1,34 @@ +import { lang, locale } from 'next/root-params' +import { connection } from 'next/server' +import { Suspense } from 'react' + +export default async function Page() { + return ( + + + + ) +} + +async function Runtime() { + await connection() + + const rootParams = await getCachedParams() + const data = await fetch( + 'https://next-data-api-endpoint.vercel.app/api/random' + ).then((res) => res.text()) + + return ( +

+ + {rootParams.lang} {rootParams.locale} + {' '} + {data} +

+ ) +} + +async function getCachedParams() { + 'use cache: private' + return { lang: await lang(), locale: await locale() } +} diff --git a/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/next.config.ts b/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/next.config.ts new file mode 100644 index 000000000000..f9ec19251f03 --- /dev/null +++ b/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/next.config.ts @@ -0,0 +1,10 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + experimental: { + useCache: true, + rootParams: true, + }, +} + +export default nextConfig diff --git a/test/e2e/app-dir/app-root-params-getters/use-cache.test.ts b/test/e2e/app-dir/app-root-params-getters/use-cache.test.ts index 3b3dcdf6cfbf..3628ce9d135b 100644 --- a/test/e2e/app-dir/app-root-params-getters/use-cache.test.ts +++ b/test/e2e/app-dir/app-root-params-getters/use-cache.test.ts @@ -5,7 +5,6 @@ import { createSandbox } from 'development-sandbox' describe('app-root-param-getters - cache - at runtime', () => { const { next, isNextDev, skipped } = nextTestSetup({ files: join(__dirname, 'fixtures', 'use-cache-runtime'), - skipStart: true, // this test asserts on build failure logs, which aren't currently observable in `next.cliOutput`. skipDeployment: true, }) @@ -39,19 +38,6 @@ describe('app-root-param-getters - cache - at runtime', () => { ) }) } else { - beforeAll(async () => { - try { - await next.start() - } catch (err) { - // if (isPPREnabled) { - // throw err - // } else { - // // in PPR/cacheComponents, we expect the build to fail, - // // so we swallow the error and let the tests assert on the logs - // } - } - }) - it('should error when using root params within a "use cache" - start', async () => { await next.render$('/en/us/use-cache') expect(next.cliOutput).toInclude( @@ -68,6 +54,30 @@ describe('app-root-param-getters - cache - at runtime', () => { } }) +describe('app-root-param-getters - private cache', () => { + const { next, isNextDev } = nextTestSetup({ + files: join(__dirname, 'fixtures', 'use-cache-private'), + }) + + if (isNextDev) { + it('should allow using root params within a "use cache: private" - dev', async () => { + await using sandbox = await createSandbox( + next, + undefined, + '/en/us/use-cache-private' + ) + const { session, browser } = sandbox + await session.assertNoRedbox() + expect(await browser.elementById('param').text()).toBe('en us') + }) + } else { + it('should allow using root params within a "use cache: private" - start', async () => { + const browser = await next.browser('/en/us/use-cache-private') + expect(await browser.elementById('param').text()).toBe('en us') + }) + } +}) + describe('app-root-param-getters - cache - at build', () => { const { next, isNextDev } = nextTestSetup({ files: join(__dirname, 'fixtures', 'use-cache-build'),