diff --git a/packages/next/src/client/app-index.tsx b/packages/next/src/client/app-index.tsx index 959b8e9e38110..bd5f0a789174b 100644 --- a/packages/next/src/client/app-index.tsx +++ b/packages/next/src/client/app-index.tsx @@ -160,6 +160,7 @@ const pendingActionQueue: Promise = new Promise( location: window.location, couldBeIntercepted: initialRSCPayload.i, postponed: initialRSCPayload.s, + prerendered: initialRSCPayload.S, }) ) ) diff --git a/packages/next/src/client/components/router-reducer/create-initial-router-state.test.tsx b/packages/next/src/client/components/router-reducer/create-initial-router-state.test.tsx index 38b8858e1aa62..ef97ec9afb952 100644 --- a/packages/next/src/client/components/router-reducer/create-initial-router-state.test.tsx +++ b/packages/next/src/client/components/router-reducer/create-initial-router-state.test.tsx @@ -43,6 +43,7 @@ describe('createInitialRouterState', () => { location: new URL('/linking', 'https://localhost') as any, couldBeIntercepted: false, postponed: false, + prerendered: false, }) const state2 = createInitialRouterState({ @@ -55,6 +56,7 @@ describe('createInitialRouterState', () => { location: new URL('/linking', 'https://localhost') as any, couldBeIntercepted: false, postponed: false, + prerendered: false, }) const expectedCache: CacheNode = { diff --git a/packages/next/src/client/components/router-reducer/create-initial-router-state.ts b/packages/next/src/client/components/router-reducer/create-initial-router-state.ts index ebfd4bc33dbdd..0ffc3d7fc33ef 100644 --- a/packages/next/src/client/components/router-reducer/create-initial-router-state.ts +++ b/packages/next/src/client/components/router-reducer/create-initial-router-state.ts @@ -17,6 +17,7 @@ export interface InitialRouterStateParameters { location: Location | null couldBeIntercepted: boolean postponed: boolean + prerendered: boolean } export function createInitialRouterState({ @@ -27,6 +28,7 @@ export function createInitialRouterState({ location, couldBeIntercepted, postponed, + prerendered, }: InitialRouterStateParameters) { // When initialized on the server, the canonical URL is provided as an array of parts. // This is to ensure that when the RSC payload streamed to the client, crawlers don't interpret it @@ -118,14 +120,13 @@ export function createInitialRouterState({ flightData: [normalizedFlightData], canonicalUrl: undefined, couldBeIntercepted: !!couldBeIntercepted, - // TODO: the server should probably send a value for this. Default to false for now. - isPrerender: false, + prerendered, postponed, }, tree: initialState.tree, prefetchCache: initialState.prefetchCache, nextUrl: initialState.nextUrl, - kind: PrefetchKind.AUTO, + kind: prerendered ? PrefetchKind.FULL : PrefetchKind.AUTO, }) } diff --git a/packages/next/src/client/components/router-reducer/fetch-server-response.ts b/packages/next/src/client/components/router-reducer/fetch-server-response.ts index d98698a16da19..4fa93362a23ee 100644 --- a/packages/next/src/client/components/router-reducer/fetch-server-response.ts +++ b/packages/next/src/client/components/router-reducer/fetch-server-response.ts @@ -23,7 +23,6 @@ import { RSC_HEADER, RSC_CONTENT_TYPE_HEADER, NEXT_HMR_REFRESH_HEADER, - NEXT_IS_PRERENDER_HEADER, NEXT_DID_POSTPONE_HEADER, } from '../app-router-headers' import { callServer } from '../../app-call-server' @@ -46,7 +45,7 @@ export type FetchServerResponseResult = { flightData: NormalizedFlightData[] | string canonicalUrl: URL | undefined couldBeIntercepted: boolean - isPrerender: boolean + prerendered: boolean postponed: boolean } @@ -72,7 +71,7 @@ function doMpaNavigation(url: string): FetchServerResponseResult { flightData: urlToUrlWithoutFlightMarker(url).toString(), canonicalUrl: undefined, couldBeIntercepted: false, - isPrerender: false, + prerendered: false, postponed: false, } } @@ -176,7 +175,6 @@ export async function fetchServerResponse( const contentType = res.headers.get('content-type') || '' const interception = !!res.headers.get('vary')?.includes(NEXT_URL) - const isPrerender = !!res.headers.get(NEXT_IS_PRERENDER_HEADER) const postponed = !!res.headers.get(NEXT_DID_POSTPONE_HEADER) let isFlightResponse = contentType.startsWith(RSC_CONTENT_TYPE_HEADER) @@ -223,7 +221,7 @@ export async function fetchServerResponse( flightData: normalizeFlightData(response.f), canonicalUrl: canonicalUrl, couldBeIntercepted: interception, - isPrerender: isPrerender, + prerendered: response.S, postponed, } } catch (err) { @@ -238,7 +236,7 @@ export async function fetchServerResponse( flightData: url.toString(), canonicalUrl: undefined, couldBeIntercepted: false, - isPrerender: false, + prerendered: false, postponed: false, } } diff --git a/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts b/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts index 621b74dc6db4f..9e353cfc23ed5 100644 --- a/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts +++ b/packages/next/src/client/components/router-reducer/prefetch-cache-utils.ts @@ -353,7 +353,7 @@ function createLazyPrefetchEntry({ // If the prefetch was a cache hit, we want to update the existing cache entry to reflect that it was a full prefetch. // This is because we know that a static response will contain the full RSC payload, and can be updated to respect the `static` // staleTime. - if (prefetchResponse.isPrerender) { + if (prefetchResponse.prerendered) { const existingCacheEntry = prefetchCache.get( // if we prefixed the cache key due to route interception, we want to use the new key. Otherwise we use the original key newCacheKey ?? prefetchCacheKey diff --git a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts index fd1f0846085c7..5d599c49c6265 100644 --- a/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts +++ b/packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts @@ -265,7 +265,7 @@ export function serverActionReducer( flightData, canonicalUrl: undefined, couldBeIntercepted: false, - isPrerender: false, + prerendered: false, postponed: false, }, tree: state.tree, diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 61e5cb3d9b1e2..7b4f969702dc9 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -460,6 +460,7 @@ async function generateDynamicRSCPayload( return { b: ctx.renderOpts.buildId, f: flightData, + S: staticGenerationStore.isStaticGeneration, } } @@ -642,6 +643,7 @@ async function getRSCPayload( m: missingSlots, G: GlobalError, s: typeof ctx.renderOpts.postponed === 'string', + S: staticGenerationStore.isStaticGeneration, } } @@ -731,6 +733,7 @@ async function getErrorRSCPayload( f: [[initialTree, initialSeedData, initialHead]], G: GlobalError, s: typeof ctx.renderOpts.postponed === 'string', + S: staticGenerationStore.isStaticGeneration, } satisfies InitialRSCPayload } @@ -767,6 +770,7 @@ function App({ location: null, couldBeIntercepted: response.i, postponed: response.s, + prerendered: response.S, }) const actionQueue = createMutableActionQueue(initialState) @@ -825,6 +829,7 @@ function AppWithoutContext({ location: null, couldBeIntercepted: response.i, postponed: response.s, + prerendered: response.S, }) const actionQueue = createMutableActionQueue(initialState) diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts index c162ff66f52b3..72efe4743ee4c 100644 --- a/packages/next/src/server/app-render/types.ts +++ b/packages/next/src/server/app-render/types.ts @@ -223,6 +223,8 @@ export type InitialRSCPayload = { G: React.ComponentType /** postponed */ s: boolean + /** prerendered */ + S: boolean } // Response from `createFromFetch` for normal rendering @@ -231,6 +233,8 @@ export type NavigationFlightResponse = { b: string /** flightData */ f: FlightData + /** prerendered */ + S: boolean } // Response from `createFromFetch` for server actions. Action's flight data can be null diff --git a/test/e2e/app-dir/app-prefetch/app/page.js b/test/e2e/app-dir/app-prefetch/app/page.js index ccded0f324b50..1f152de4f44cc 100644 --- a/test/e2e/app-dir/app-prefetch/app/page.js +++ b/test/e2e/app-dir/app-prefetch/app/page.js @@ -8,6 +8,9 @@ export default function HomePage() { To Static Page + + To Dynamic Page + To Dynamic Slug Page diff --git a/test/e2e/app-dir/app-prefetch/prefetching.test.ts b/test/e2e/app-dir/app-prefetch/prefetching.test.ts index 4858f76ff0ade..c7c52dda7b4fd 100644 --- a/test/e2e/app-dir/app-prefetch/prefetching.test.ts +++ b/test/e2e/app-dir/app-prefetch/prefetching.test.ts @@ -308,9 +308,11 @@ describe('app dir - prefetching', () => { beforePageLoad(page: Page) { page.on('request', async (req: Request) => { const url = new URL(req.url()) - const headers = await req.allHeaders() - if (headers['rsc']) { - rscRequests.push(url.pathname) + if (url.pathname === '/static-page' || url.pathname === '/') { + const headers = await req.allHeaders() + if (headers['rsc']) { + rscRequests.push(url.pathname) + } } }) }, @@ -331,6 +333,27 @@ describe('app dir - prefetching', () => { 0 ) }) + + // navigate to index + await browser.elementByCss('[href="/"]').click() + + // we should be on the index page + await browser.waitForElementByCss('#to-dashboard') + + // navigate to the static page + await browser.elementByCss('[href="/static-page"]').click() + + // we should be on the static page + await browser.waitForElementByCss('#static-page') + + await browser.waitForIdleNetwork() + + // We still shouldn't see any requests since it respects the static staletime (default 5m) + await retry(async () => { + expect(rscRequests.filter((req) => req === '/static-page').length).toBe( + 0 + ) + }) }) it('should not re-fetch the initial dynamic page if the same page is prefetched with prefetch={true}', async () => { @@ -339,9 +362,11 @@ describe('app dir - prefetching', () => { beforePageLoad(page: Page) { page.on('request', async (req: Request) => { const url = new URL(req.url()) - const headers = await req.allHeaders() - if (headers['rsc']) { - rscRequests.push(url.pathname) + if (url.pathname === '/dynamic-page' || url.pathname === '/') { + const headers = await req.allHeaders() + if (headers['rsc']) { + rscRequests.push(url.pathname) + } } }) }, @@ -362,6 +387,27 @@ describe('app dir - prefetching', () => { rscRequests.filter((req) => req === '/dynamic-page').length ).toBe(0) }) + + // navigate to index + await browser.elementByCss('[href="/"]').click() + + // we should be on the index page + await browser.waitForElementByCss('#to-dashboard') + + // navigate to the dynamic page + await browser.elementByCss('[href="/dynamic-page"]').click() + + // we should be on the dynamic page + await browser.waitForElementByCss('#dynamic-page') + + await browser.waitForIdleNetwork() + + // We should see a request for the dynamic page since it respects the dynamic staletime (default 0) + await retry(async () => { + expect( + rscRequests.filter((req) => req === '/dynamic-page').length + ).toBe(1) + }) }) }) diff --git a/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts b/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts index 5355fb7d1c602..a35b3258afcfd 100644 --- a/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts +++ b/test/e2e/app-dir/revalidatetag-rsc/revalidatetag-rsc.test.ts @@ -27,12 +27,16 @@ describe('revalidateTag-rsc', () => { await browser.refresh() const randomNumber2 = await browser.elementById('data').text() expect(randomNumber).toEqual(randomNumber2) - await browser.elementByCss('#revalidate-via-page').click() await browser.waitForElementByCss('#home') await browser.elementByCss('#home').click() await browser.waitForElementByCss('#data') - const randomNumber3 = await browser.elementById('data').text() - expect(randomNumber3).not.toEqual(randomNumber) + await retry(async () => { + // need to refresh to evict client router cache + await browser.refresh() + await browser.waitForElementByCss('#data') + const randomNumber3 = await browser.elementById('data').text() + expect(randomNumber3).not.toEqual(randomNumber) + }) }) })