From 63837aa11ba53d1dd93505fdca8f3c0253dc7117 Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Wed, 15 Oct 2025 23:59:05 +0200 Subject: [PATCH 1/3] label as Prefetch/Prefetchable depending on prefetch config --- .../next/src/server/app-render/app-render.tsx | 18 +++++++++-- .../server/app-render/staged-validation.tsx | 32 +++++++++++++++++++ .../cache-components.dev-warmup.test.ts | 24 ++++++++------ 3 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 packages/next/src/server/app-render/staged-validation.tsx diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 4b8474f852949..3ff4108f5f542 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -212,6 +212,7 @@ import { createPromiseWithResolvers } from '../../shared/lib/promise-with-resolv import { ImageConfigContext } from '../../shared/lib/image-config-context.shared-runtime' import { imageConfigDefault } from '../../shared/lib/image-config' import { RenderStage, StagedRenderingController } from './staged-rendering' +import { anySegmentHasRuntimePrefetchEnabled } from './staged-validation' export type GetDynamicParamFromSegment = ( // [slug] / [[slug]] / [...slug] @@ -2768,7 +2769,16 @@ async function renderWithRestartOnCacheMissInDev( getPayload: (requestStore: RequestStore) => Promise, onError: (error: unknown) => void ) { - const { htmlRequestId, renderOpts, requestId } = ctx + const { + htmlRequestId, + renderOpts, + requestId, + componentMod: { + routeModule: { + userland: { loaderTree }, + }, + }, + } = ctx const { clientReferenceManifest, ComponentMod, @@ -2777,6 +2787,9 @@ async function renderWithRestartOnCacheMissInDev( } = renderOpts assertClientReferenceManifest(clientReferenceManifest) + const hasRuntimePrefetch = + await anySegmentHasRuntimePrefetchEnabled(loaderTree) + // If the render is restarted, we'll recreate a fresh request store let requestStore: RequestStore = initialRequestStore @@ -2786,8 +2799,7 @@ async function renderWithRestartOnCacheMissInDev( case RenderStage.Static: return 'Prerender' case RenderStage.Runtime: - // TODO: only label as "Prefetch" if the page has a `prefetch` config. - return 'Prefetch' + return hasRuntimePrefetch ? 'Prefetch' : 'Prefetchable' case RenderStage.Dynamic: return 'Server' default: diff --git a/packages/next/src/server/app-render/staged-validation.tsx b/packages/next/src/server/app-render/staged-validation.tsx new file mode 100644 index 0000000000000..6e71905c17305 --- /dev/null +++ b/packages/next/src/server/app-render/staged-validation.tsx @@ -0,0 +1,32 @@ +import { getLayoutOrPageModule } from '../lib/app-dir-module' +import type { LoaderTree } from '../lib/app-dir-module' +import { parseLoaderTree } from '../../shared/lib/router/utils/parse-loader-tree' +import type { AppSegmentConfig } from '../../build/segment-config/app/app-segment-config' + +export async function anySegmentHasRuntimePrefetchEnabled( + tree: LoaderTree +): Promise { + const { mod: layoutOrPageMod } = await getLayoutOrPageModule(tree) + + // TODO(restart-on-cache-miss): Does this work correctly for client page/layout modules? + const prefetchConfig = layoutOrPageMod + ? (layoutOrPageMod as AppSegmentConfig).unstable_prefetch + : undefined + /** Whether this segment should use a runtime prefetch instead of a static prefetch. */ + const hasRuntimePrefetch = prefetchConfig?.mode === 'runtime' + if (hasRuntimePrefetch) { + return true + } + + const { parallelRoutes } = parseLoaderTree(tree) + for (const parallelRouteKey in parallelRoutes) { + const parallelRoute = parallelRoutes[parallelRouteKey] + const hasChildRuntimePrefetch = + await anySegmentHasRuntimePrefetchEnabled(parallelRoute) + if (hasChildRuntimePrefetch) { + return true + } + } + + return false +} diff --git a/test/development/app-dir/cache-components-dev-warmup/cache-components.dev-warmup.test.ts b/test/development/app-dir/cache-components-dev-warmup/cache-components.dev-warmup.test.ts index 92e8cd292dfa1..70be555ab8d90 100644 --- a/test/development/app-dir/cache-components-dev-warmup/cache-components.dev-warmup.test.ts +++ b/test/development/app-dir/cache-components-dev-warmup/cache-components.dev-warmup.test.ts @@ -29,7 +29,7 @@ describe('cache-components-dev-warmup', () => { ) { // Match logs that contain the message, with any environment. const logPattern = new RegExp( - `^(?=.*\\b${message}\\b)(?=.*\\b(Cache|Prerender|Prefetch|Server)\\b).*` + `^(?=.*\\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)) @@ -175,12 +175,12 @@ describe('cache-components-dev-warmup', () => { // Private caches are dynamic holes in static prerenders, // so they shouldn't resolve in the static stage. - assertLog(logs, 'after private cache read - page', 'Prefetch') - assertLog(logs, 'after private cache read - layout', 'Prefetch') + assertLog(logs, 'after private cache read - page', 'Prefetchable') + assertLog(logs, 'after private cache read - layout', 'Prefetchable') assertLog( logs, 'after successive private cache reads - page', - 'Prefetch' + 'Prefetchable' ) assertLog(logs, 'after uncached fetch - layout', 'Server') @@ -204,8 +204,12 @@ describe('cache-components-dev-warmup', () => { // 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', 'Prefetch') - assertLog(logs, 'after short-lived cache read - layout', 'Prefetch') + assertLog(logs, 'after short-lived cache read - page', 'Prefetchable') + assertLog( + logs, + 'after short-lived cache read - layout', + 'Prefetchable' + ) assertLog(logs, 'after uncached fetch - layout', 'Server') assertLog(logs, 'after uncached fetch - page', 'Server') @@ -247,10 +251,10 @@ describe('cache-components-dev-warmup', () => { 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`, 'Prefetch') - assertLog(logs, `after headers`, 'Prefetch') - assertLog(logs, `after params`, 'Prefetch') - assertLog(logs, `after searchParams`, 'Prefetch') + assertLog(logs, `after cookies`, 'Prefetchable') + assertLog(logs, `after headers`, 'Prefetchable') + assertLog(logs, `after params`, 'Prefetchable') + assertLog(logs, `after searchParams`, 'Prefetchable') assertLog(logs, 'after connection', 'Server') } From e1a436816d2c84df4f33cf47c30e8e2fb8d6e61b Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Mon, 20 Oct 2025 00:24:01 +0200 Subject: [PATCH 2/3] tests --- .../cache-components.dev-warmup.test.ts | 498 ++++++++++-------- .../app/apis/[param]/page.tsx | 49 ++ .../app/data-fetching.tsx | 0 .../with-prefetch-config}/app/layout.tsx | 0 .../with-prefetch-config}/app/page.tsx | 0 .../app/private-cache/data-fetching.tsx | 0 .../app/private-cache/layout.tsx | 29 + .../app/private-cache/page.tsx | 0 .../app/revalidate/route.ts | 0 .../app/short-lived-cache/data-fetching.tsx | 0 .../app/short-lived-cache/layout.tsx | 29 + .../app/short-lived-cache/page.tsx | 0 .../app/simple/layout.tsx | 26 + .../app/simple/loading.tsx | 0 .../with-prefetch-config}/app/simple/page.tsx | 0 .../app/successive-caches/page.tsx | 46 ++ .../with-prefetch-config}/next.config.ts | 0 .../app/apis/[param]/page.tsx | 0 .../app/data-fetching.tsx | 103 ++++ .../without-prefetch-config/app/layout.tsx | 7 + .../without-prefetch-config/app/page.tsx | 26 + .../app/private-cache/data-fetching.tsx | 55 ++ .../app/private-cache/layout.tsx | 0 .../app/private-cache/page.tsx | 27 + .../app/revalidate/route.ts | 8 + .../app/short-lived-cache/data-fetching.tsx | 25 + .../app/short-lived-cache/layout.tsx | 0 .../app/short-lived-cache/page.tsx | 23 + .../app/simple/layout.tsx | 0 .../app/simple/loading.tsx | 10 + .../app/simple/page.tsx | 26 + .../app/successive-caches/page.tsx | 0 .../without-prefetch-config/next.config.ts | 9 + 33 files changed, 769 insertions(+), 227 deletions(-) create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/apis/[param]/page.tsx rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/with-prefetch-config}/app/data-fetching.tsx (100%) rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/with-prefetch-config}/app/layout.tsx (100%) rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/with-prefetch-config}/app/page.tsx (100%) rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/with-prefetch-config}/app/private-cache/data-fetching.tsx (100%) create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/private-cache/layout.tsx rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/with-prefetch-config}/app/private-cache/page.tsx (100%) rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/with-prefetch-config}/app/revalidate/route.ts (100%) rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/with-prefetch-config}/app/short-lived-cache/data-fetching.tsx (100%) create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/short-lived-cache/layout.tsx rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/with-prefetch-config}/app/short-lived-cache/page.tsx (100%) create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/simple/layout.tsx rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/with-prefetch-config}/app/simple/loading.tsx (100%) rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/with-prefetch-config}/app/simple/page.tsx (100%) create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/successive-caches/page.tsx rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/with-prefetch-config}/next.config.ts (100%) rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/without-prefetch-config}/app/apis/[param]/page.tsx (100%) create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/data-fetching.tsx create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/layout.tsx create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/page.tsx create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/private-cache/data-fetching.tsx rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/without-prefetch-config}/app/private-cache/layout.tsx (100%) create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/private-cache/page.tsx create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/revalidate/route.ts create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/short-lived-cache/data-fetching.tsx rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/without-prefetch-config}/app/short-lived-cache/layout.tsx (100%) create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/short-lived-cache/page.tsx rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/without-prefetch-config}/app/simple/layout.tsx (100%) create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/simple/loading.tsx create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/simple/page.tsx rename test/development/app-dir/cache-components-dev-warmup/{ => fixtures/without-prefetch-config}/app/successive-caches/page.tsx (100%) create mode 100644 test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/next.config.ts diff --git a/test/development/app-dir/cache-components-dev-warmup/cache-components.dev-warmup.test.ts b/test/development/app-dir/cache-components-dev-warmup/cache-components.dev-warmup.test.ts index 70be555ab8d90..6f28637cfacdb 100644 --- a/test/development/app-dir/cache-components-dev-warmup/cache-components.dev-warmup.test.ts +++ b/test/development/app-dir/cache-components-dev-warmup/cache-components.dev-warmup.test.ts @@ -1,238 +1,305 @@ 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('cache-components-dev-warmup', () => { - const { next, isTurbopack } = nextTestSetup({ - files: __dirname, - }) - - // 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 - } +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), + }) - 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')}` + // 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), + ]) } - // 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 + ) { + const browser = await next.browser(path) - async function testInitialLoad( - path: string, - assertLogs: (browser: Playwright) => Promise - ) { - const browser = await next.browser(path) - - // Initial load. - await retry(() => assertLogs(browser)) - - // 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)) - - 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 - // - // 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 - } + // Initial load. + await retry(() => assertLogs(browser)) - // After a revalidation the subsequent warmup render must discard stale - // cache entries. - // This should not affect the environment labels. - await revalidatePath(path) + // We should not see any errors related to the aborted render. + expect(next.cliOutput).not.toContain( + 'AbortError: This operation was aborted' + ) - await browser.loadPage(next.url + path) // clears old logs - await retry(() => assertLogs(browser)) - } + // 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)) - async function testNavigation( - path: string, - assertLogs: (browser: Playwright) => Promise - ) { - const browser = await next.browser('/') - - // Initial nav (first time loading the page) - await browser.elementByCss(`a[href="${path}"]`).click() - await retry(() => assertLogs(browser)) - - // 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)) - - 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 - // - // 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 - } + expect(next.cliOutput).not.toContain( + 'AbortError: This operation was aborted' + ) - // After a revalidation the subsequent warmup render must discard stale - // cache entries. - // This should not affect the environment labels. - await revalidatePath(path) + 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 + // + // 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 + } - await browser.loadPage(next.url + '/') // clears old logs - await browser.elementByCss(`a[href="${path}"]`).click() - await retry(() => assertLogs(browser)) - } + // 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)) - 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}` + // We should not see any errors related to the aborted render. + expect(next.cliOutput).not.toContain( + 'AbortError: This operation was aborted' ) } - } - 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') + async function testNavigation( + path: string, + assertLogs: (browser: Playwright) => Promise + ) { + const browser = await next.browser('/') - assertLog(logs, 'after uncached fetch - layout', 'Server') - assertLog(logs, 'after uncached fetch - page', 'Server') - } + // Initial nav (first time loading the page) + await browser.elementByCss(`a[href="${path}"]`).click() + await retry(() => assertLogs(browser)) - if (isInitialLoad) { - await testInitialLoad(path, assertLogs) - } else { - await testNavigation(path, assertLogs) - } - }) + // We should not see any errors related to the aborted render. + expect(next.cliOutput).not.toContain( + 'AbortError: This operation was aborted' + ) - it('cached data + private cache', async () => { - const path = '/private-cache' + // 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)) - const assertLogs = async (browser: Playwright) => { - const logs = await browser.log() - assertLog(logs, 'after cache read - layout', 'Prerender') - assertLog(logs, 'after cache read - page', 'Prerender') + expect(next.cliOutput).not.toContain( + 'AbortError: This operation was aborted' + ) - // Private caches are dynamic holes in static prerenders, - // so they shouldn't resolve in the static stage. - assertLog(logs, 'after private cache read - page', 'Prefetchable') - assertLog(logs, 'after private cache read - layout', 'Prefetchable') - assertLog( - logs, - 'after successive private cache reads - page', - 'Prefetchable' - ) - - assertLog(logs, 'after uncached fetch - layout', 'Server') - assertLog(logs, 'after uncached fetch - page', 'Server') - } + 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 + // + // 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 + } - if (isInitialLoad) { - await testInitialLoad(path, assertLogs) - } else { - await testNavigation(path, assertLogs) - } - }) + // After a revalidation the subsequent warmup render must discard stale + // cache entries. + // This should not affect the environment labels. + await revalidatePath(path) - it('cached data + short-lived cached data', async () => { - const path = '/short-lived-cache' + await browser.loadPage(next.url + '/') // clears old logs + await browser.elementByCss(`a[href="${path}"]`).click() + await retry(() => assertLogs(browser)) - const assertLogs = async (browser: Playwright) => { - const logs = await browser.log() - assertLog(logs, 'after cache read - layout', 'Prerender') - assertLog(logs, 'after cache read - page', 'Prerender') + expect(next.cliOutput).not.toContain( + 'AbortError: This operation was aborted' + ) + } - // 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', 'Prefetchable') - assertLog( - logs, - 'after short-lived cache read - layout', - 'Prefetchable' - ) - - assertLog(logs, 'after uncached fetch - layout', 'Server') - assertLog(logs, 'after uncached fetch - page', 'Server') - } + 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}` + ) + } + } - if (isInitialLoad) { - await testInitialLoad(path, assertLogs) - } else { - await testNavigation(path, assertLogs) - } + 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('cache reads that reveal more components with more caches', async () => { - const path = '/successive-caches' + it('request APIs resolve in the correct phase', async () => { + const path = '/apis/123' 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') + 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) { @@ -242,28 +309,5 @@ describe('cache-components-dev-warmup', () => { } }) }) - - 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`, 'Prefetchable') - assertLog(logs, `after headers`, 'Prefetchable') - assertLog(logs, `after params`, 'Prefetchable') - assertLog(logs, `after searchParams`, 'Prefetchable') - - assertLog(logs, 'after connection', 'Server') - } - - if (isInitialLoad) { - await testInitialLoad(path, assertLogs) - } else { - await testNavigation(path, assertLogs) - } - }) - }) -}) + } +) diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/apis/[param]/page.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/apis/[param]/page.tsx new file mode 100644 index 0000000000000..75fa21cd00000 --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/apis/[param]/page.tsx @@ -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_prefetch = { mode: 'runtime', samples: [{}] } + +const CACHE_KEY = __dirname + '/__PAGE__' + +export default function Page({ params, searchParams }) { + return ( +
+

+ This page checks whether runtime/dynamic APIs resolve in the correct + stage (regardless of whether we had a cache miss or not) +

+ + connection()} /> + + {/* Runtime */} + cookies()} /> + headers()} /> + params} /> + searchParams} /> + {/* Dynamic */} + connection()} /> +
+ ) +} + +function LogAfter({ label, api }: { label: string; api: () => Promise }) { + return ( + Waiting for {label}...}> + + + ) +} + +async function LogAfterInner({ + label, + api, +}: { + label: string + api: () => Promise +}) { + await api() + console.log(`after ${label}`) + return
Finished {label}
+} diff --git a/test/development/app-dir/cache-components-dev-warmup/app/data-fetching.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/data-fetching.tsx similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/app/data-fetching.tsx rename to test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/data-fetching.tsx diff --git a/test/development/app-dir/cache-components-dev-warmup/app/layout.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/layout.tsx similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/app/layout.tsx rename to test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/layout.tsx diff --git a/test/development/app-dir/cache-components-dev-warmup/app/page.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/page.tsx similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/app/page.tsx rename to test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/page.tsx diff --git a/test/development/app-dir/cache-components-dev-warmup/app/private-cache/data-fetching.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/private-cache/data-fetching.tsx similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/app/private-cache/data-fetching.tsx rename to test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/private-cache/data-fetching.tsx diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/private-cache/layout.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/private-cache/layout.tsx new file mode 100644 index 0000000000000..e0129cb9cd280 --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/private-cache/layout.tsx @@ -0,0 +1,29 @@ +import { Suspense } from 'react' +import { UncachedFetch, CachedData } from '../data-fetching' +import { PrivateCachedData } from './data-fetching' + +export const unstable_prefetch = { mode: 'runtime', samples: [{}] } + +const CACHE_KEY = '/private-cache/__LAYOUT__' + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + <> + {children} +
+

Layout

+

This data is from a layout

+ + + + + + + + + + +
+ + ) +} diff --git a/test/development/app-dir/cache-components-dev-warmup/app/private-cache/page.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/private-cache/page.tsx similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/app/private-cache/page.tsx rename to test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/private-cache/page.tsx diff --git a/test/development/app-dir/cache-components-dev-warmup/app/revalidate/route.ts b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/revalidate/route.ts similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/app/revalidate/route.ts rename to test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/revalidate/route.ts diff --git a/test/development/app-dir/cache-components-dev-warmup/app/short-lived-cache/data-fetching.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/short-lived-cache/data-fetching.tsx similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/app/short-lived-cache/data-fetching.tsx rename to test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/short-lived-cache/data-fetching.tsx diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/short-lived-cache/layout.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/short-lived-cache/layout.tsx new file mode 100644 index 0000000000000..8007d5b2bb2df --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/short-lived-cache/layout.tsx @@ -0,0 +1,29 @@ +import { Suspense } from 'react' +import { UncachedFetch, CachedData } from '../data-fetching' +import { ShortLivedCache } from './data-fetching' + +export const unstable_prefetch = { mode: 'runtime', samples: [{}] } + +const CACHE_KEY = __dirname + '/__LAYOUT__' + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + <> + {children} +
+

Layout

+

This data is from a layout

+ + + + + + + + + + +
+ + ) +} diff --git a/test/development/app-dir/cache-components-dev-warmup/app/short-lived-cache/page.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/short-lived-cache/page.tsx similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/app/short-lived-cache/page.tsx rename to test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/short-lived-cache/page.tsx diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/simple/layout.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/simple/layout.tsx new file mode 100644 index 0000000000000..3aa99c3dc40c3 --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/simple/layout.tsx @@ -0,0 +1,26 @@ +import { Suspense } from 'react' +import { UncachedFetch, CachedFetch, CachedData } from '../data-fetching' + +export const unstable_prefetch = { mode: 'runtime', samples: [{}] } + +const CACHE_KEY = __dirname + '/__LAYOUT__' + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + <> + {children} +
+

Layout

+

This data is from a layout

+ + + + + + + + +
+ + ) +} diff --git a/test/development/app-dir/cache-components-dev-warmup/app/simple/loading.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/simple/loading.tsx similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/app/simple/loading.tsx rename to test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/simple/loading.tsx diff --git a/test/development/app-dir/cache-components-dev-warmup/app/simple/page.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/simple/page.tsx similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/app/simple/page.tsx rename to test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/simple/page.tsx diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/successive-caches/page.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/successive-caches/page.tsx new file mode 100644 index 0000000000000..e50993a0bcc0b --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/app/successive-caches/page.tsx @@ -0,0 +1,46 @@ +export const unstable_prefetch = { mode: 'runtime', samples: [{}] } + +export default async function Page() { + return ( +
+

Warmup Dev Renders - deep successive cache reads

+ +
+ ) +} + +async function One() { + const cache1 = await fastCache() + console.log('after cache 1') + return +} + +async function Two({ cache1 }) { + const cache2 = await slowCache(1) + console.log('after cache 2') + return +} + +async function Three({ cache1, cache2 }) { + console.log('after caches 1 and 2') + const cache3 = await slowCache(2) + console.log('after cache 3') + return ( +
+
Cache 1: {cache1}
+
Cache 2: {cache2}
+
Cache 3: {cache3}
+
+ ) +} + +async function fastCache() { + 'use cache' + return Math.random() +} + +async function slowCache(_key: number) { + 'use cache' + await new Promise((resolve) => setTimeout(resolve)) + return Math.random() +} diff --git a/test/development/app-dir/cache-components-dev-warmup/next.config.ts b/test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/next.config.ts similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/next.config.ts rename to test/development/app-dir/cache-components-dev-warmup/fixtures/with-prefetch-config/next.config.ts diff --git a/test/development/app-dir/cache-components-dev-warmup/app/apis/[param]/page.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/apis/[param]/page.tsx similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/app/apis/[param]/page.tsx rename to test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/apis/[param]/page.tsx diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/data-fetching.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/data-fetching.tsx new file mode 100644 index 0000000000000..d12d987dc1aba --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/data-fetching.tsx @@ -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 ( +
+
Cached Data
+
{data}
+
+ ) +} + +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 ( +
+
Cached Data (successive reads)
+
{data1}
+
+ +
+
+ ) +} + +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 ( +
+
Cached Fetch
+
{data}
+
+ ) +} + +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 ( +
+
Uncached Fetch
+
{data}
+
+ ) +} diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/layout.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/layout.tsx new file mode 100644 index 0000000000000..e7077399c03ce --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/layout.tsx @@ -0,0 +1,7 @@ +export default function Root({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/page.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/page.tsx new file mode 100644 index 0000000000000..7e0011ce547ac --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/page.tsx @@ -0,0 +1,26 @@ +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 ( +
+
    +
  • + /simple +
  • +
  • + /private-cache +
  • +
  • + /short-lived-cache +
  • +
  • + /successive-caches +
  • +
  • + /apis/123 +
  • +
+
+ ) +} diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/private-cache/data-fetching.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/private-cache/data-fetching.tsx new file mode 100644 index 0000000000000..61a8c92ed02d3 --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/private-cache/data-fetching.tsx @@ -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 ( +
+
Private Cached Data (Page)
+
{data}
+
+ ) +} + +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 ( +
+
Private Cached Data (successive reads)
+
{data1}
+
+ +
+
+ ) +} + +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() +} diff --git a/test/development/app-dir/cache-components-dev-warmup/app/private-cache/layout.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/private-cache/layout.tsx similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/app/private-cache/layout.tsx rename to test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/private-cache/layout.tsx diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/private-cache/page.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/private-cache/page.tsx new file mode 100644 index 0000000000000..1f586f5025b45 --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/private-cache/page.tsx @@ -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 ( +
+

Warmup Dev Renders - private cache

+ + + + + + + + + + + + + + +
+ ) +} diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/revalidate/route.ts b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/revalidate/route.ts new file mode 100644 index 0000000000000..d246995676e4b --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/revalidate/route.ts @@ -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 }) +} diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/short-lived-cache/data-fetching.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/short-lived-cache/data-fetching.tsx new file mode 100644 index 0000000000000..5fd697350a929 --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/short-lived-cache/data-fetching.tsx @@ -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 ( +
+
Short-lived Cached Data (Page)
+
{data}
+
+ ) +} + +async function getShortLivedCachedData(_key: string) { + 'use cache' + cacheLife('seconds') + await new Promise((r) => setTimeout(r)) + return Math.random() +} diff --git a/test/development/app-dir/cache-components-dev-warmup/app/short-lived-cache/layout.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/short-lived-cache/layout.tsx similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/app/short-lived-cache/layout.tsx rename to test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/short-lived-cache/layout.tsx diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/short-lived-cache/page.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/short-lived-cache/page.tsx new file mode 100644 index 0000000000000..3f1569860d924 --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/short-lived-cache/page.tsx @@ -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 ( +
+

Warmup Dev Renders - short lived cache

+ + + + + + + + + + +
+ ) +} diff --git a/test/development/app-dir/cache-components-dev-warmup/app/simple/layout.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/simple/layout.tsx similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/app/simple/layout.tsx rename to test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/simple/layout.tsx diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/simple/loading.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/simple/loading.tsx new file mode 100644 index 0000000000000..7711287aaa504 --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/simple/loading.tsx @@ -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
loading...
+} diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/simple/page.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/simple/page.tsx new file mode 100644 index 0000000000000..47b574f89dd25 --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/simple/page.tsx @@ -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 ( +
+

Warmup Dev Renders

+ + + + + + + + + +
+ ) +} diff --git a/test/development/app-dir/cache-components-dev-warmup/app/successive-caches/page.tsx b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/successive-caches/page.tsx similarity index 100% rename from test/development/app-dir/cache-components-dev-warmup/app/successive-caches/page.tsx rename to test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/app/successive-caches/page.tsx diff --git a/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/next.config.ts b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/next.config.ts new file mode 100644 index 0000000000000..3970d95d89d9c --- /dev/null +++ b/test/development/app-dir/cache-components-dev-warmup/fixtures/without-prefetch-config/next.config.ts @@ -0,0 +1,9 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + experimental: { + cacheComponents: true, + }, +} + +export default nextConfig From 45c90487ab296eedf5bb4afc9884bb81daef819b Mon Sep 17 00:00:00 2001 From: Josh Story Date: Sun, 19 Oct 2025 23:28:55 -0700 Subject: [PATCH 3/3] Fix more tests --- .../e2e/app-dir/server-source-maps/server-source-maps.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/e2e/app-dir/server-source-maps/server-source-maps.test.ts b/test/e2e/app-dir/server-source-maps/server-source-maps.test.ts index 93cb73ba19b13..14cd3782097b8 100644 --- a/test/e2e/app-dir/server-source-maps/server-source-maps.test.ts +++ b/test/e2e/app-dir/server-source-maps/server-source-maps.test.ts @@ -480,8 +480,6 @@ describe('app-dir - server source maps', () => { "{module evaluation} app/module-evaluation/module.js (1:22)", "{module evaluation} app/module-evaluation/page.js (1:1)", "{module evaluation} app/module-evaluation/page.js (6:1)", - "Array.map ", - "Function.all ", "Page ", ], } @@ -522,8 +520,6 @@ describe('app-dir - server source maps', () => { "", "eval about:/Prerender/webpack-internal:///(rsc)/app/module-evaluation/page.js (5:65)", "", - "Function.all ", - "Function.all ", "Page ", ], }