From 86f6a76c5530f2e2ccbf13cbeee03c4840a4f907 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Fri, 29 Sep 2023 13:11:55 -0700 Subject: [PATCH 01/11] Revert "Revert: "Generate prefetch RSC payload during build for SSR paths (#54403)" (#56059)" This reverts commit 4cfb0acd7210ccda7a06f6e24c8e52f4fce33e6c. --- packages/next/src/build/index.ts | 21 ++++++ packages/next/src/export/routes/app-page.ts | 67 ++++++++++++++++++- packages/next/src/export/worker.ts | 6 +- packages/next/src/server/base-server.ts | 27 ++++++++ .../e2e/app-dir/app-static/app-static.test.ts | 18 +++++ test/e2e/app-dir/app/index.test.ts | 55 +++++++++++++++ .../custom-routes/test/index.test.js | 1 + .../dynamic-routing/test/index.test.js | 1 + 8 files changed, 194 insertions(+), 2 deletions(-) diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 3a25959f00a25..72f9c7f5ae160 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -129,6 +129,7 @@ import { eventSwcPlugins } from '../telemetry/events/swc-plugins' import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' import { ACTION, + NEXT_ROUTER_PREFETCH, RSC, RSC_CONTENT_TYPE_HEADER, RSC_VARY_HEADER, @@ -227,6 +228,7 @@ export type RoutesManifest = { rsc: { header: typeof RSC varyHeader: typeof RSC_VARY_HEADER + prefetchHeader: typeof NEXT_ROUTER_PREFETCH } skipMiddlewareUrlNormalize?: boolean caseSensitive?: boolean @@ -795,6 +797,7 @@ export default async function build( rsc: { header: RSC, varyHeader: RSC_VARY_HEADER, + prefetchHeader: NEXT_ROUTER_PREFETCH, contentTypeHeader: RSC_CONTENT_TYPE_HEADER, }, skipMiddlewareUrlNormalize: config.skipMiddlewareUrlNormalize, @@ -1055,6 +1058,7 @@ export default async function build( const additionalSsgPaths = new Map>() const additionalSsgPathsEncoded = new Map>() const appStaticPaths = new Map>() + const appPrefetchPaths = new Map() const appStaticPathsEncoded = new Map>() const appNormalizedPaths = new Map() const appDynamicParamPaths = new Set() @@ -1554,6 +1558,14 @@ export default async function build( appDynamicParamPaths.add(originalAppPath) } appDefaultConfigs.set(originalAppPath, appConfig) + + if ( + !isStatic && + !isAppRouteRoute(originalAppPath) && + !isDynamicRoute(originalAppPath) + ) { + appPrefetchPaths.set(originalAppPath, page) + } } } else { if (isEdgeRuntime(pageRuntime)) { @@ -2001,6 +2013,15 @@ export default async function build( }) }) + for (const [originalAppPath, page] of appPrefetchPaths) { + defaultMap[page] = { + page: originalAppPath, + query: {}, + _isAppDir: true, + _isAppPrefetch: true, + } + } + if (i18n) { for (const page of [ ...staticPages, diff --git a/packages/next/src/export/routes/app-page.ts b/packages/next/src/export/routes/app-page.ts index 4624f0bfecf61..cdce84635a061 100644 --- a/packages/next/src/export/routes/app-page.ts +++ b/packages/next/src/export/routes/app-page.ts @@ -6,6 +6,11 @@ import type { NextParsedUrlQuery } from '../../server/request-meta' import fs from 'fs/promises' import { MockedRequest, MockedResponse } from '../../server/lib/mock-request' +import { + RSC, + NEXT_URL, + NEXT_ROUTER_PREFETCH, +} from '../../client/components/app-router-headers' import { isDynamicUsageError } from '../helpers/is-dynamic-usage-error' import { NEXT_CACHE_TAGS_HEADER } from '../../lib/constants' import { hasNextSupport } from '../../telemetry/ci-info' @@ -19,6 +24,41 @@ const render: AppPageRender = (...args) => { ) } +export async function generatePrefetchRsc( + req: MockedRequest, + path: string, + res: MockedResponse, + pathname: string, + query: NextParsedUrlQuery, + htmlFilepath: string, + renderOpts: RenderOpts +) { + req.headers[RSC.toLowerCase()] = '1' + req.headers[NEXT_URL.toLowerCase()] = path + req.headers[NEXT_ROUTER_PREFETCH.toLowerCase()] = '1' + + renderOpts.supportsDynamicHTML = true + delete renderOpts.isRevalidate + + const prefetchRenderResult = await render( + req, + res, + pathname, + query, + renderOpts + ) + + prefetchRenderResult.pipe(res) + await res.hasStreamed + + const prefetchRscData = Buffer.concat(res.buffers) + + await fs.writeFile( + htmlFilepath.replace(/\.html$/, '.prefetch.rsc'), + prefetchRscData + ) +} + export async function exportAppPage( req: MockedRequest, res: MockedResponse, @@ -29,7 +69,8 @@ export async function exportAppPage( renderOpts: RenderOpts, htmlFilepath: string, debugOutput: boolean, - isDynamicError: boolean + isDynamicError: boolean, + isAppPrefetch: boolean ): Promise { // If the page is `/_not-found`, then we should update the page to be `/404`. if (page === '/_not-found') { @@ -37,6 +78,20 @@ export async function exportAppPage( } try { + if (isAppPrefetch) { + await generatePrefetchRsc( + req, + path, + res, + pathname, + query, + htmlFilepath, + renderOpts + ) + + return { fromBuildExportRevalidate: 0 } + } + const result = await render(req, res, pathname, query, renderOpts) const html = result.toUnchunkedString() const { metadata } = result @@ -50,6 +105,16 @@ export async function exportAppPage( ) } + await generatePrefetchRsc( + req, + path, + res, + pathname, + query, + htmlFilepath, + renderOpts + ) + const { staticBailoutInfo = {} } = metadata if (revalidate === 0 && debugOutput && staticBailoutInfo?.description) { diff --git a/packages/next/src/export/worker.ts b/packages/next/src/export/worker.ts index 43f8cd604114b..e8d49e88d34a0 100644 --- a/packages/next/src/export/worker.ts +++ b/packages/next/src/export/worker.ts @@ -108,6 +108,9 @@ async function exportPageImpl(input: ExportPageInput) { // Check if this is an `app/` page. _isAppDir: isAppDir = false, + // Check if this is an `app/` prefix request. + _isAppPrefetch: isAppPrefetch = false, + // Check if this should error when dynamic usage is detected. _isDynamicError: isDynamicError = false, @@ -306,7 +309,8 @@ async function exportPageImpl(input: ExportPageInput) { renderOpts, htmlFilepath, debugOutput, - isDynamicError + isDynamicError, + isAppPrefetch ) } diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 0021ff6cf89cf..99e4f188bcba4 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -86,6 +86,8 @@ import { FLIGHT_PARAMETERS, NEXT_RSC_UNION_QUERY, ACTION, + NEXT_ROUTER_PREFETCH, + RSC_CONTENT_TYPE_HEADER, } from '../client/components/app-router-headers' import { MatchOptions, @@ -2124,6 +2126,31 @@ export default abstract class Server { } else if ( components.routeModule?.definition.kind === RouteKind.APP_PAGE ) { + const isAppPrefetch = req.headers[NEXT_ROUTER_PREFETCH.toLowerCase()] + + if ( + isAppPrefetch && + ssgCacheKey && + process.env.NODE_ENV === 'production' + ) { + try { + const prefetchRsc = await this.getPrefetchRsc(ssgCacheKey) + + if (prefetchRsc) { + res.setHeader( + 'cache-control', + 'private, no-cache, no-store, max-age=0, must-revalidate' + ) + res.setHeader('content-type', RSC_CONTENT_TYPE_HEADER) + res.body(prefetchRsc).send() + return null + } + } catch (_) { + // we fallback to invoking the function if prefetch + // data is not available + } + } + const module = components.routeModule as AppPageRouteModule // Due to the way we pass data by mutating `renderOpts`, we can't extend the diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 4d97bebc25c5b..2fa85426c9f41 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -505,9 +505,11 @@ createNextDescribe( 'stale-cache-serving-edge/app-page/page.js', 'stale-cache-serving-edge/app-page/page_client-reference-manifest.js', 'stale-cache-serving-edge/route-handler/route.js', + 'stale-cache-serving/app-page.prefetch.rsc', 'stale-cache-serving/app-page/page.js', 'stale-cache-serving/app-page/page_client-reference-manifest.js', 'stale-cache-serving/route-handler/route.js', + 'custom.prefetch.rsc', 'force-cache/page.js', 'ssg-draft-mode.html', '(new)/custom/page.js', @@ -523,17 +525,23 @@ createNextDescribe( 'force-static/first.html', 'force-static/second.rsc', 'ssg-draft-mode/test.rsc', + 'ssr-forced.prefetch.rsc', 'blog/seb/second-post.rsc', 'blog/tim/first-post.html', 'force-static/second.html', 'ssg-draft-mode/test.html', 'blog/seb/second-post.html', + 'force-static.prefetch.rsc', 'ssg-draft-mode/test-2.rsc', 'blog/styfle/first-post.rsc', + 'default-cache.prefetch.rsc', 'dynamic-error/[id]/page.js', + 'response-url.prefetch.rsc', 'ssg-draft-mode/test-2.html', 'blog/styfle/first-post.html', 'blog/styfle/second-post.rsc', + 'fetch-no-cache.prefetch.rsc', + 'force-no-store.prefetch.rsc', 'force-static/[slug]/page.js', 'hooks/use-pathname/slug.rsc', 'hooks/use-search-params.rsc', @@ -559,9 +567,11 @@ createNextDescribe( 'react-fetch-deduping-node/page.js', 'variable-revalidate/encoding.html', 'variable-revalidate/cookie/page.js', + 'gen-params-dynamic/one.prefetch.rsc', 'ssg-draft-mode/[[...route]]/page.js', 'variable-revalidate/post-method.rsc', 'dynamic-no-gen-params/[slug]/page.js', + 'ssr-auto/cache-no-store.prefetch.rsc', 'static-to-dynamic-error/[id]/page.js', 'variable-revalidate/encoding/page.js', 'variable-revalidate/no-store/page.js', @@ -575,6 +585,7 @@ createNextDescribe( 'variable-revalidate/revalidate-3.html', 'force-dynamic-prerender/[slug]/page.js', 'gen-params-dynamic-revalidate/one.html', + 'react-fetch-deduping-node.prefetch.rsc', 'ssr-auto/fetch-revalidate-zero/page.js', 'variable-revalidate/authorization.html', 'force-dynamic-no-prerender/[id]/page.js', @@ -585,6 +596,7 @@ createNextDescribe( 'partial-gen-params/[lang]/[slug]/page.js', 'variable-revalidate/headers-instance.rsc', 'variable-revalidate/revalidate-3/page.js', + 'force-dynamic-catch-all/slug.prefetch.rsc', 'hooks/use-search-params/force-static.html', 'hooks/use-search-params/with-suspense.rsc', 'route-handler/revalidate-360-isr/route.js', @@ -594,8 +606,10 @@ createNextDescribe( 'variable-revalidate/headers-instance.html', 'hooks/use-search-params/with-suspense.html', 'route-handler-edge/revalidate-360/route.js', + 'variable-revalidate/no-store.prefetch.rsc', 'variable-revalidate/revalidate-360-isr.rsc', 'variable-revalidate/revalidate-360/page.js', + 'ssr-auto/fetch-revalidate-zero.prefetch.rsc', 'static-to-dynamic-error-forced/[id]/page.js', 'variable-config-revalidate/revalidate-3.rsc', 'variable-revalidate/revalidate-360-isr.html', @@ -605,6 +619,7 @@ createNextDescribe( 'variable-config-revalidate/revalidate-3.html', 'variable-revalidate-edge/post-method/page.js', 'variable-revalidate/headers-instance/page.js', + 'variable-revalidate/status-code.prefetch.rsc', 'force-cache/page_client-reference-manifest.js', 'hooks/use-search-params/with-suspense/page.js', 'variable-revalidate-edge/revalidate-3/page.js', @@ -614,8 +629,10 @@ createNextDescribe( 'variable-revalidate/revalidate-360-isr/page.js', 'blog/[author]/page_client-reference-manifest.js', 'default-cache/page_client-reference-manifest.js', + 'force-dynamic-prerender/frameworks.prefetch.rsc', 'variable-config-revalidate/revalidate-3/page.js', 'variable-revalidate/post-method-request/page.js', + 'variable-revalidate/revalidate-360.prefetch.rsc', 'fetch-no-cache/page_client-reference-manifest.js', 'force-dynamic-catch-all/[slug]/[[...id]]/page.js', 'force-no-store/page_client-reference-manifest.js', @@ -644,6 +661,7 @@ createNextDescribe( 'partial-gen-params-no-additional-lang/fr/second.html', 'partial-gen-params-no-additional-slug/en/second.html', 'partial-gen-params-no-additional-slug/fr/second.html', + 'variable-revalidate/post-method-request.prefetch.rsc', 'variable-revalidate-edge/post-method-request/page.js', 'force-static/[slug]/page_client-reference-manifest.js', 'blog/[author]/[slug]/page_client-reference-manifest.js', diff --git a/test/e2e/app-dir/app/index.test.ts b/test/e2e/app-dir/app/index.test.ts index 40134754d9bf9..77ad55ddb6853 100644 --- a/test/e2e/app-dir/app/index.test.ts +++ b/test/e2e/app-dir/app/index.test.ts @@ -2,6 +2,8 @@ import { createNextDescribe } from 'e2e-utils' import { check, getRedboxHeader, hasRedbox, waitFor } from 'next-test-utils' import cheerio from 'cheerio' import stripAnsi from 'strip-ansi' +import { BrowserInterface } from 'test/lib/browsers/base' +import { Request } from 'playwright-core' createNextDescribe( 'app dir', @@ -10,6 +12,59 @@ createNextDescribe( }, ({ next, isNextDev: isDev, isNextStart, isNextDeploy }) => { if (isNextStart) { + it('should use RSC prefetch data from build', async () => { + expect( + await next.readFile('.next/server/app/linking.prefetch.rsc') + ).toBeTruthy() + expect( + await next.readFile('.next/server/app/linking/about.prefetch.rsc') + ).toContain('About loading...') + expect( + await next.readFile( + '.next/server/app/dashboard/deployments/breakdown.prefetch.rsc' + ) + ).toBeTruthy() + expect( + await next + .readFile( + '.next/server/app/dashboard/deployments/[id].prefetch.rsc' + ) + .catch(() => false) + ).toBeFalsy() + + const outputStart = next.cliOutput.length + const browser: BrowserInterface = await next.browser('/') + const rscReqs = [] + + browser.on('request', (req: Request) => { + if (req.headers()['rsc']) { + rscReqs.push(req.url()) + } + }) + + await browser.eval('window.location.href = "/linking"') + + await check(async () => { + return rscReqs.length > 3 ? 'success' : JSON.stringify(rscReqs) + }, 'success') + + const trimmedOutput = next.cliOutput.substring(outputStart) + + expect(trimmedOutput).not.toContain( + 'rendering dashboard/(custom)/deployments/breakdown' + ) + expect(trimmedOutput).not.toContain( + 'rendering /dashboard/deployments/[id]' + ) + expect(trimmedOutput).not.toContain('rendering linking about page') + + await browser.elementByCss('#breakdown').click() + await check( + () => next.cliOutput.substring(outputStart), + /rendering .*breakdown/ + ) + }) + it('should have correct size in build output', async () => { expect(next.cliOutput).toMatch( /\/dashboard\/another.*? [^0]{1,} [\w]{1,}B/ diff --git a/test/integration/custom-routes/test/index.test.js b/test/integration/custom-routes/test/index.test.js index 58f2e9347b987..569c7809b53a6 100644 --- a/test/integration/custom-routes/test/index.test.js +++ b/test/integration/custom-routes/test/index.test.js @@ -2660,6 +2660,7 @@ const runTests = (isDev = false, isTurbo = false) => { contentTypeHeader: 'text/x-component', varyHeader: 'RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url', + prefetchHeader: 'Next-Router-Prefetch', }, }) }) diff --git a/test/integration/dynamic-routing/test/index.test.js b/test/integration/dynamic-routing/test/index.test.js index 5e39562e43b98..704a7e10dd890 100644 --- a/test/integration/dynamic-routing/test/index.test.js +++ b/test/integration/dynamic-routing/test/index.test.js @@ -1494,6 +1494,7 @@ function runTests({ dev }) { contentTypeHeader: 'text/x-component', varyHeader: 'RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url', + prefetchHeader: 'Next-Router-Prefetch', }, }) }) From 55f8af4f306ffd1cead6a0208e464abf5934ae9d Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Fri, 29 Sep 2023 13:13:40 -0700 Subject: [PATCH 02/11] Revert "app router: ensure static prefetch renders loading.js (#55950)" This reverts commit ca57258dbd4d5b3aba119088e2c188bf0f4671bc. --- packages/next/src/server/app-render/app-render.tsx | 8 ++------ test/e2e/app-dir/app/app/linking/about/loading.js | 3 --- test/e2e/app-dir/app/index.test.ts | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) delete mode 100644 test/e2e/app-dir/app/app/linking/about/loading.js diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 2553c2273087a..752e8f299bdfe 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -1122,9 +1122,7 @@ export const renderToHTMLOrFlight: AppPageRender = ( getDynamicParamFromSegment, query ), - isPrefetch && - !Boolean(components.loading) && - !hasLoadingComponentInTree(loaderTree) + isPrefetch && !Boolean(components.loading) ? null : // Create component tree using the slice of the loaderTree // @ts-expect-error TODO-APP: fix async component type @@ -1147,9 +1145,7 @@ export const renderToHTMLOrFlight: AppPageRender = ( return }), - isPrefetch && - !Boolean(components.loading) && - !hasLoadingComponentInTree(loaderTree) + isPrefetch && !Boolean(components.loading) ? null : (() => { const { layoutOrPagePath } = diff --git a/test/e2e/app-dir/app/app/linking/about/loading.js b/test/e2e/app-dir/app/app/linking/about/loading.js deleted file mode 100644 index 3d9aa325a9cf7..0000000000000 --- a/test/e2e/app-dir/app/app/linking/about/loading.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function AboutLoading() { - return

About loading...

-} diff --git a/test/e2e/app-dir/app/index.test.ts b/test/e2e/app-dir/app/index.test.ts index 77ad55ddb6853..97d88cbd2e793 100644 --- a/test/e2e/app-dir/app/index.test.ts +++ b/test/e2e/app-dir/app/index.test.ts @@ -18,7 +18,7 @@ createNextDescribe( ).toBeTruthy() expect( await next.readFile('.next/server/app/linking/about.prefetch.rsc') - ).toContain('About loading...') + ).toBeTruthy() expect( await next.readFile( '.next/server/app/dashboard/deployments/breakdown.prefetch.rsc' From e7fb3623ff42fac53e5498b6b51858300f54f087 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Fri, 29 Sep 2023 14:30:19 -0700 Subject: [PATCH 03/11] add prefetch-auto test case and fix prefetch bailout logic --- .../next/src/server/app-render/app-render.tsx | 11 ++- .../app/prefetch-auto/[slug]/layout.js | 15 ++++ .../app/prefetch-auto/[slug]/loading.js | 3 + .../app/prefetch-auto/[slug]/page.js | 21 ++++++ .../app-dir/app-prefetch/prefetching.test.ts | 68 +++++++++++++++++++ .../app-dir/app/app/linking/about/loading.js | 3 + test/e2e/app-dir/app/index.test.ts | 2 +- 7 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/layout.js create mode 100644 test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/loading.js create mode 100644 test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/page.js create mode 100644 test/e2e/app-dir/app/app/linking/about/loading.js diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 752e8f299bdfe..e846b283192d2 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -1106,6 +1106,13 @@ export const renderToHTMLOrFlight: AppPageRender = ( // Explicit refresh flightRouterState[3] === 'refetch' + const shouldSkipComponentTree = + isPrefetch && + !Boolean(components.loading) && + (flightRouterState || + // If there is no flightRouterState, we need to check the entire loader tree, as otherwise we'll be only checking the root + !hasLoadingComponentInTree(loaderTree)) + if (!parentRendered && renderComponentsOnThisLevel) { const overriddenSegment = flightRouterState && @@ -1122,7 +1129,7 @@ export const renderToHTMLOrFlight: AppPageRender = ( getDynamicParamFromSegment, query ), - isPrefetch && !Boolean(components.loading) + shouldSkipComponentTree ? null : // Create component tree using the slice of the loaderTree // @ts-expect-error TODO-APP: fix async component type @@ -1145,7 +1152,7 @@ export const renderToHTMLOrFlight: AppPageRender = ( return }), - isPrefetch && !Boolean(components.loading) + shouldSkipComponentTree ? null : (() => { const { layoutOrPagePath } = diff --git a/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/layout.js b/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/layout.js new file mode 100644 index 0000000000000..a9123d41fc185 --- /dev/null +++ b/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/layout.js @@ -0,0 +1,15 @@ +import Link from 'next/link' + +export const dynamic = 'force-dynamic' + +export default async function Layout({ children }) { + return ( +
+

Layout

+ + Prefetch Link + + {children} +
+ ) +} diff --git a/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/loading.js b/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/loading.js new file mode 100644 index 0000000000000..5b91c2379fa9c --- /dev/null +++ b/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/loading.js @@ -0,0 +1,3 @@ +export default function Loading() { + return

Loading Prefetch Auto

+} diff --git a/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/page.js b/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/page.js new file mode 100644 index 0000000000000..b6aa1a5033af3 --- /dev/null +++ b/test/e2e/app-dir/app-prefetch/app/prefetch-auto/[slug]/page.js @@ -0,0 +1,21 @@ +export const dynamic = 'force-dynamic' + +function getData() { + const res = new Promise((resolve) => { + setTimeout(() => { + resolve({ message: 'Hello World!' }) + }, 2000) + }) + return res +} + +export default async function Page({ params }) { + const result = await getData() + + return ( + <> +

{JSON.stringify(params)}

+

{JSON.stringify(result)}

+ + ) +} diff --git a/test/e2e/app-dir/app-prefetch/prefetching.test.ts b/test/e2e/app-dir/app-prefetch/prefetching.test.ts index d1d315c7437d7..61007037ce16d 100644 --- a/test/e2e/app-dir/app-prefetch/prefetching.test.ts +++ b/test/e2e/app-dir/app-prefetch/prefetching.test.ts @@ -232,5 +232,73 @@ createNextDescribe( await browser.elementByCss('#prefetch-false-page-result').text() ).toBe('Result page') }) + + it('should not need to prefetch the layout if the prefetch is initiated at the same segment', async () => { + const stateTree = encodeURIComponent( + JSON.stringify([ + '', + { + children: [ + 'prefetch-auto', + { + children: [ + ['slug', 'justputit', 'd'], + { children: ['__PAGE__', {}] }, + ], + }, + ], + }, + null, + null, + true, + ]) + ) + const response = await next.fetch(`/prefetch-auto/justputit?_rsc=dcqtr`, { + headers: { + RSC: '1', + 'Next-Router-Prefetch': '1', + 'Next-Router-State-Tree': stateTree, + 'Next-Url': '/prefetch-auto/justputit', + }, + }) + + const prefetchResponse = await response.text() + expect(prefetchResponse).not.toContain('Hello World') + expect(prefetchResponse).not.toContain('Loading Prefetch Auto') + }) + + it('should only prefetch the loading state and not the component tree when prefetching at the same segment', async () => { + const stateTree = encodeURIComponent( + JSON.stringify([ + '', + { + children: [ + 'prefetch-auto', + { + children: [ + ['slug', 'vercel', 'd'], + { children: ['__PAGE__', {}] }, + ], + }, + ], + }, + null, + null, + true, + ]) + ) + const response = await next.fetch(`/prefetch-auto/justputit?_rsc=dcqtr`, { + headers: { + RSC: '1', + 'Next-Router-Prefetch': '1', + 'Next-Router-State-Tree': stateTree, + 'Next-Url': '/prefetch-auto/vercel', + }, + }) + + const prefetchResponse = await response.text() + expect(prefetchResponse).not.toContain('Hello World') + expect(prefetchResponse).toContain('Loading Prefetch Auto') + }) } ) diff --git a/test/e2e/app-dir/app/app/linking/about/loading.js b/test/e2e/app-dir/app/app/linking/about/loading.js new file mode 100644 index 0000000000000..3d9aa325a9cf7 --- /dev/null +++ b/test/e2e/app-dir/app/app/linking/about/loading.js @@ -0,0 +1,3 @@ +export default function AboutLoading() { + return

About loading...

+} diff --git a/test/e2e/app-dir/app/index.test.ts b/test/e2e/app-dir/app/index.test.ts index 97d88cbd2e793..77ad55ddb6853 100644 --- a/test/e2e/app-dir/app/index.test.ts +++ b/test/e2e/app-dir/app/index.test.ts @@ -18,7 +18,7 @@ createNextDescribe( ).toBeTruthy() expect( await next.readFile('.next/server/app/linking/about.prefetch.rsc') - ).toBeTruthy() + ).toContain('About loading...') expect( await next.readFile( '.next/server/app/dashboard/deployments/breakdown.prefetch.rsc' From 6ea3f690910da8dd0bb700e961bd2de153e0bfdc Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Fri, 29 Sep 2023 18:01:12 -0700 Subject: [PATCH 04/11] don't generate prefetches if layout opts into dynamic rendering --- ...tatic-generation-async-storage.external.ts | 1 + .../components/static-generation-bailout.ts | 4 ++++ packages/next/src/export/routes/app-page.ts | 3 +++ packages/next/src/server/app-render/types.ts | 1 + ...static-generation-async-storage-wrapper.ts | 6 ++++++ packages/next/src/server/render.tsx | 1 + .../(protected)/bar/page.js | 9 ++++++++ .../(protected)/foo/page.js | 3 +++ .../(protected)/layout.js | 21 +++++++++++++++++++ .../(protected)/loading.js | 3 +++ .../(protected)/page.js | 3 +++ .../(protected)/redirect.js | 18 ++++++++++++++++ .../app-dir/app-prefetch/prefetching.test.ts | 20 ++++++++++++++++++ 13 files changed, 93 insertions(+) create mode 100644 test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/bar/page.js create mode 100644 test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/foo/page.js create mode 100644 test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/layout.js create mode 100644 test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/loading.js create mode 100644 test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/page.js create mode 100644 test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/redirect.js diff --git a/packages/next/src/client/components/static-generation-async-storage.external.ts b/packages/next/src/client/components/static-generation-async-storage.external.ts index 95c1195bd0248..6cc9b9d6e2d4a 100644 --- a/packages/next/src/client/components/static-generation-async-storage.external.ts +++ b/packages/next/src/client/components/static-generation-async-storage.external.ts @@ -6,6 +6,7 @@ import { createAsyncLocalStorage } from './async-local-storage' export interface StaticGenerationStore { readonly isStaticGeneration: boolean + readonly isStaticPrefetch?: boolean readonly pagePath?: string readonly urlPathname: string readonly incrementalCache?: IncrementalCache diff --git a/packages/next/src/client/components/static-generation-bailout.ts b/packages/next/src/client/components/static-generation-bailout.ts index 4d35150664251..438f55a61284b 100644 --- a/packages/next/src/client/components/static-generation-bailout.ts +++ b/packages/next/src/client/components/static-generation-bailout.ts @@ -30,6 +30,10 @@ export const staticGenerationBailout: StaticGenerationBailout = ( return true } + if (staticGenerationStore?.isStaticPrefetch) { + staticGenerationStore.dynamicUsageDescription = reason + } + if (staticGenerationStore?.dynamicShouldError) { throw new StaticGenBailoutError( formatErrorMessage(reason, { ...opts, dynamic: opts?.dynamic ?? 'error' }) diff --git a/packages/next/src/export/routes/app-page.ts b/packages/next/src/export/routes/app-page.ts index cdce84635a061..cff16d5ca1155 100644 --- a/packages/next/src/export/routes/app-page.ts +++ b/packages/next/src/export/routes/app-page.ts @@ -38,6 +38,7 @@ export async function generatePrefetchRsc( req.headers[NEXT_ROUTER_PREFETCH.toLowerCase()] = '1' renderOpts.supportsDynamicHTML = true + renderOpts.isPrefetch = true delete renderOpts.isRevalidate const prefetchRenderResult = await render( @@ -53,6 +54,8 @@ export async function generatePrefetchRsc( const prefetchRscData = Buffer.concat(res.buffers) + if ((renderOpts as any).store.dynamicUsageDescription) return + await fs.writeFile( htmlFilepath.replace(/\.html$/, '.prefetch.rsc'), prefetchRscData diff --git a/packages/next/src/server/app-render/types.ts b/packages/next/src/server/app-render/types.ts index 9345d114b620f..1d83b5f417a77 100644 --- a/packages/next/src/server/app-render/types.ts +++ b/packages/next/src/server/app-render/types.ts @@ -131,6 +131,7 @@ export type RenderOptsPartial = { ) => Promise serverActionsBodySizeLimit?: SizeLimit params?: ParsedUrlQuery + isPrefetch?: boolean } export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial diff --git a/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts b/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts index e590b6301bfbf..c01736aabbec5 100644 --- a/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts +++ b/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts @@ -2,6 +2,7 @@ import type { AsyncStorageWrapper } from './async-storage-wrapper' import type { StaticGenerationStore } from '../../client/components/static-generation-async-storage.external' import type { AsyncLocalStorage } from 'async_hooks' import type { IncrementalCache } from '../lib/incremental-cache' +import { PHASE_PRODUCTION_BUILD } from '../../shared/lib/constants' export type StaticGenerationContext = { urlPathname: string @@ -12,6 +13,7 @@ export type StaticGenerationContext = { isRevalidate?: boolean isOnDemandRevalidate?: boolean isBot?: boolean + isPrefetch?: boolean nextExport?: boolean fetchCache?: StaticGenerationStore['fetchCache'] isDraftMode?: boolean @@ -63,6 +65,10 @@ export const StaticGenerationAsyncStorageWrapper: AsyncStorageWrapper< const store: StaticGenerationStore = { isStaticGeneration, + isStaticPrefetch: Boolean( + process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD && + renderOpts.isPrefetch + ), urlPathname, pagePath: renderOpts.originalPathname, incrementalCache: diff --git a/packages/next/src/server/render.tsx b/packages/next/src/server/render.tsx index 7a67f4d40d462..aa9d6b657be6b 100644 --- a/packages/next/src/server/render.tsx +++ b/packages/next/src/server/render.tsx @@ -283,6 +283,7 @@ export type RenderOptsPartial = { deploymentId?: string isServerAction?: boolean isExperimentalCompile?: boolean + isPrefetch?: boolean } export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial diff --git a/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/bar/page.js b/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/bar/page.js new file mode 100644 index 0000000000000..8fe9938081170 --- /dev/null +++ b/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/bar/page.js @@ -0,0 +1,9 @@ +import Link from 'next/link' + +export default function Page() { + return ( +
+ To Foo +
+ ) +} diff --git a/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/foo/page.js b/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/foo/page.js new file mode 100644 index 0000000000000..39e14f6d18636 --- /dev/null +++ b/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/foo/page.js @@ -0,0 +1,3 @@ +export default function FooPage() { + return

Foo page

+} diff --git a/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/layout.js b/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/layout.js new file mode 100644 index 0000000000000..2f392fe39e896 --- /dev/null +++ b/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/layout.js @@ -0,0 +1,21 @@ +import { cookies } from 'next/headers' +import { Redirect } from './redirect' + +async function isLoggedIn() { + // sleep for 1s + await new Promise((resolve) => setTimeout(resolve, 1000)) + + const cookieData = cookies() + const hasSession = !!cookieData.get('logged-in') + + return hasSession +} + +export default async function Layout({ children }) { + const loggedIn = await isLoggedIn() + console.log({ loggedIn }) + + if (!loggedIn) return + + return
Protected Layout: {children}
+} diff --git a/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/loading.js b/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/loading.js new file mode 100644 index 0000000000000..b3e23741a9c3c --- /dev/null +++ b/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/loading.js @@ -0,0 +1,3 @@ +export default function ProtectedLoading() { + return

Protected loading...

+} diff --git a/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/page.js b/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/page.js new file mode 100644 index 0000000000000..eafdefefd4d2e --- /dev/null +++ b/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/page.js @@ -0,0 +1,3 @@ +export default function ProtectedPage() { + return

Protected Pgae

+} diff --git a/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/redirect.js b/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/redirect.js new file mode 100644 index 0000000000000..ab476f9e99d3b --- /dev/null +++ b/test/e2e/app-dir/app-prefetch/app/prefetch-dynamic-usage/(protected)/redirect.js @@ -0,0 +1,18 @@ +'use client' + +import { usePathname, useRouter } from 'next/navigation' +import { useEffect } from 'react' + +export function Redirect() { + const path = usePathname() + const router = useRouter() + + useEffect(() => { + const nextUrl = encodeURIComponent( + path ? decodeURIComponent(path) : '/search' + ) + router.push(`/login?nextUrl=${nextUrl}`) + }, [path, router]) + + return null +} diff --git a/test/e2e/app-dir/app-prefetch/prefetching.test.ts b/test/e2e/app-dir/app-prefetch/prefetching.test.ts index 61007037ce16d..2d9b476690893 100644 --- a/test/e2e/app-dir/app-prefetch/prefetching.test.ts +++ b/test/e2e/app-dir/app-prefetch/prefetching.test.ts @@ -300,5 +300,25 @@ createNextDescribe( expect(prefetchResponse).not.toContain('Hello World') expect(prefetchResponse).toContain('Loading Prefetch Auto') }) + + it('should not generate static prefetches for layouts that opt into dynamic rendering', async () => { + await next.stop() + const rootLoading = await next.readFile('./app/loading.js') + await next.deleteFile('./app/loading.js') + await next.start() + expect( + await next + .readFile('.next/server/app/prefetch-dynamic-usage/foo.prefetch.rsc') + .catch(() => false) + ).toBeFalsy() + + expect( + await next + .readFile('.next/server/app/prefetch-dynamic-usage/foo.prefetch.rsc') + .catch(() => false) + ).toBeFalsy() + + await next.patchFile('./app/loading', rootLoading) + }) } ) From b1bf5e727ca3bcc6e405d0cf5a6ffa735c771a25 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Sat, 30 Sep 2023 00:08:56 -0700 Subject: [PATCH 05/11] fix test --- test/e2e/app-dir/app/index.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/app-dir/app/index.test.ts b/test/e2e/app-dir/app/index.test.ts index 4d1c17c2a67c9..26f6ba749c6fa 100644 --- a/test/e2e/app-dir/app/index.test.ts +++ b/test/e2e/app-dir/app/index.test.ts @@ -14,7 +14,7 @@ createNextDescribe( : undefined, }, ({ next, isNextDev: isDev, isNextStart, isNextDeploy }) => { - if (isNextStart) { + if (isNextStart && !process.env.NEXT_EXPERIMENTAL_COMPILE) { it('should use RSC prefetch data from build', async () => { expect( await next.readFile('.next/server/app/linking.prefetch.rsc') From e8e5f8a5909a4ed516175b99e9d8345b405c4365 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Sat, 30 Sep 2023 15:32:52 -0700 Subject: [PATCH 06/11] fix static prefetches when running next start & add a failing test --- packages/next/src/server/base-server.ts | 8 +--- .../app-prefetch-i18n.test.ts | 39 +++++++++++++++++++ .../(default)/dynamic-area/[slug]/page.js | 3 ++ .../app/[region]/(default)/layout.js | 13 +++++++ .../(default)/static-prefetch/page.js | 9 +++++ .../app-dir/app-prefetch-i18n/app/layout.js | 17 ++++++++ .../e2e/app-dir/app-prefetch-i18n/app/page.js | 3 ++ 7 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 test/e2e/app-dir/app-prefetch-i18n/app-prefetch-i18n.test.ts create mode 100644 test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/dynamic-area/[slug]/page.js create mode 100644 test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/layout.js create mode 100644 test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/static-prefetch/page.js create mode 100644 test/e2e/app-dir/app-prefetch-i18n/app/layout.js create mode 100644 test/e2e/app-dir/app-prefetch-i18n/app/page.js diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 8ac83edd05368..810aa60137803 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -2128,13 +2128,9 @@ export default abstract class Server { ) { const isAppPrefetch = req.headers[NEXT_ROUTER_PREFETCH.toLowerCase()] - if ( - isAppPrefetch && - ssgCacheKey && - process.env.NODE_ENV === 'production' - ) { + if (isAppPrefetch && process.env.NODE_ENV === 'production') { try { - const prefetchRsc = await this.getPrefetchRsc(ssgCacheKey) + const prefetchRsc = await this.getPrefetchRsc(resolvedUrlPathname) if (prefetchRsc) { res.setHeader( diff --git a/test/e2e/app-dir/app-prefetch-i18n/app-prefetch-i18n.test.ts b/test/e2e/app-dir/app-prefetch-i18n/app-prefetch-i18n.test.ts new file mode 100644 index 0000000000000..9e35759f11ee9 --- /dev/null +++ b/test/e2e/app-dir/app-prefetch-i18n/app-prefetch-i18n.test.ts @@ -0,0 +1,39 @@ +import { createNextDescribe } from '../../../lib/e2e-utils' +import { waitFor } from '../../../lib/next-test-utils' + +createNextDescribe( + 'app-prefetch-i18n', + { + files: __dirname, + }, + ({ next, isNextDev }) => { + if (isNextDev) { + it('should skip next dev', () => {}) + return + } + + it('should correctly navigate between static & dynamic pages', async () => { + const browser = await next.browser('/') + // Ensure the page is prefetched + await waitFor(1000) + + await browser.elementByCss('#static-prefetch').click() + + expect(await browser.elementByCss('#static-prefetch-page').text()).toBe( + 'Hello from Static Page' + ) + + await browser.elementByCss('#dynamic-prefetch').click() + + expect(await browser.elementByCss('#dynamic-prefetch-page').text()).toBe( + 'Hello from Dynamic Page' + ) + + await browser.elementByCss('#static-prefetch').click() + + expect(await browser.elementByCss('#static-prefetch-page').text()).toBe( + 'Hello from Static Page' + ) + }) + } +) diff --git a/test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/dynamic-area/[slug]/page.js b/test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/dynamic-area/[slug]/page.js new file mode 100644 index 0000000000000..ac5e4d8294eb3 --- /dev/null +++ b/test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/dynamic-area/[slug]/page.js @@ -0,0 +1,3 @@ +export default async function DynamicPage({ params, searchParams }) { + return
Hello from Dynamic Page
+} diff --git a/test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/layout.js b/test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/layout.js new file mode 100644 index 0000000000000..92c69396f61f7 --- /dev/null +++ b/test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/layout.js @@ -0,0 +1,13 @@ +export const regions = ['SE', 'DE'] + +export default async function Layout({ children, params }) { + return children +} + +export function generateStaticParams() { + return regions.map((region) => ({ + region, + })) +} + +export const dynamicParams = false diff --git a/test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/static-prefetch/page.js b/test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/static-prefetch/page.js new file mode 100644 index 0000000000000..837a9cff1fd86 --- /dev/null +++ b/test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/static-prefetch/page.js @@ -0,0 +1,9 @@ +export const dynamic = 'force-dynamic' + +export default async function StaticPrefetchPage({ params }) { + return ( +
+

Hello from Static Page

+
+ ) +} diff --git a/test/e2e/app-dir/app-prefetch-i18n/app/layout.js b/test/e2e/app-dir/app-prefetch-i18n/app/layout.js new file mode 100644 index 0000000000000..d169da575e505 --- /dev/null +++ b/test/e2e/app-dir/app-prefetch-i18n/app/layout.js @@ -0,0 +1,17 @@ +import Link from 'next/link' +export default function RootLayout({ children }) { + return ( + + + {children} + + + Static Prefetch + + + Dynamic Prefetch + + + + ) +} diff --git a/test/e2e/app-dir/app-prefetch-i18n/app/page.js b/test/e2e/app-dir/app-prefetch-i18n/app/page.js new file mode 100644 index 0000000000000..f35d2ac8dabcc --- /dev/null +++ b/test/e2e/app-dir/app-prefetch-i18n/app/page.js @@ -0,0 +1,3 @@ +export default function Main() { + return
Hey
+} From c50657fb21812e4db8b6869d0a5a8b00e98884f7 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Sat, 30 Sep 2023 17:00:31 -0700 Subject: [PATCH 07/11] ensure cache key is cased consistently --- .../router-reducer/create-router-cache-key.ts | 2 +- packages/next/src/export/routes/app-page.ts | 11 +---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/next/src/client/components/router-reducer/create-router-cache-key.ts b/packages/next/src/client/components/router-reducer/create-router-cache-key.ts index 8f272dc5abf24..3e9d739a717d7 100644 --- a/packages/next/src/client/components/router-reducer/create-router-cache-key.ts +++ b/packages/next/src/client/components/router-reducer/create-router-cache-key.ts @@ -5,7 +5,7 @@ export function createRouterCacheKey( withoutSearchParameters: boolean = false ) { return Array.isArray(segment) - ? `${segment[0]}|${segment[1]}|${segment[2]}` + ? `${segment[0]}|${segment[1]}|${segment[2]}`.toLowerCase() : withoutSearchParameters && segment.startsWith('__PAGE__') ? '__PAGE__' : segment diff --git a/packages/next/src/export/routes/app-page.ts b/packages/next/src/export/routes/app-page.ts index cff16d5ca1155..02a7144aa47bd 100644 --- a/packages/next/src/export/routes/app-page.ts +++ b/packages/next/src/export/routes/app-page.ts @@ -29,7 +29,6 @@ export async function generatePrefetchRsc( path: string, res: MockedResponse, pathname: string, - query: NextParsedUrlQuery, htmlFilepath: string, renderOpts: RenderOpts ) { @@ -41,13 +40,7 @@ export async function generatePrefetchRsc( renderOpts.isPrefetch = true delete renderOpts.isRevalidate - const prefetchRenderResult = await render( - req, - res, - pathname, - query, - renderOpts - ) + const prefetchRenderResult = await render(req, res, pathname, {}, renderOpts) prefetchRenderResult.pipe(res) await res.hasStreamed @@ -87,7 +80,6 @@ export async function exportAppPage( path, res, pathname, - query, htmlFilepath, renderOpts ) @@ -113,7 +105,6 @@ export async function exportAppPage( path, res, pathname, - query, htmlFilepath, renderOpts ) From 917e6d15e270ca608d550426b3224d1b5271718d Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Sun, 1 Oct 2023 14:03:28 -0700 Subject: [PATCH 08/11] use a separate flag to determine if we can statically prefetch --- ...tatic-generation-async-storage.external.ts | 1 + .../components/static-generation-bailout.ts | 10 ++++++---- packages/next/src/export/routes/app-page.ts | 20 ++++++++++--------- .../e2e/app-dir/app-static/app-static.test.ts | 1 - 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/next/src/client/components/static-generation-async-storage.external.ts b/packages/next/src/client/components/static-generation-async-storage.external.ts index 6cc9b9d6e2d4a..256e7457c1127 100644 --- a/packages/next/src/client/components/static-generation-async-storage.external.ts +++ b/packages/next/src/client/components/static-generation-async-storage.external.ts @@ -31,6 +31,7 @@ export interface StaticGenerationStore { dynamicUsageDescription?: string dynamicUsageStack?: string dynamicUsageErr?: DynamicServerError + staticPrefetchBailout?: boolean nextFetchId?: number pathWasRevalidated?: boolean diff --git a/packages/next/src/client/components/static-generation-bailout.ts b/packages/next/src/client/components/static-generation-bailout.ts index 438f55a61284b..3363fd7b5d3a8 100644 --- a/packages/next/src/client/components/static-generation-bailout.ts +++ b/packages/next/src/client/components/static-generation-bailout.ts @@ -30,10 +30,6 @@ export const staticGenerationBailout: StaticGenerationBailout = ( return true } - if (staticGenerationStore?.isStaticPrefetch) { - staticGenerationStore.dynamicUsageDescription = reason - } - if (staticGenerationStore?.dynamicShouldError) { throw new StaticGenBailoutError( formatErrorMessage(reason, { ...opts, dynamic: opts?.dynamic ?? 'error' }) @@ -42,6 +38,12 @@ export const staticGenerationBailout: StaticGenerationBailout = ( if (staticGenerationStore) { staticGenerationStore.revalidate = 0 + + if (!opts?.dynamic) { + // we can statically prefetch pages that opt into dynamic, + // but not things like headers/cookies + staticGenerationStore.staticPrefetchBailout = true + } } if (staticGenerationStore?.isStaticGeneration) { diff --git a/packages/next/src/export/routes/app-page.ts b/packages/next/src/export/routes/app-page.ts index 02a7144aa47bd..30680e8ecd65e 100644 --- a/packages/next/src/export/routes/app-page.ts +++ b/packages/next/src/export/routes/app-page.ts @@ -47,7 +47,7 @@ export async function generatePrefetchRsc( const prefetchRscData = Buffer.concat(res.buffers) - if ((renderOpts as any).store.dynamicUsageDescription) return + if ((renderOpts as any).store.staticPrefetchBailout) return await fs.writeFile( htmlFilepath.replace(/\.html$/, '.prefetch.rsc'), @@ -100,14 +100,16 @@ export async function exportAppPage( ) } - await generatePrefetchRsc( - req, - path, - res, - pathname, - htmlFilepath, - renderOpts - ) + if (!(renderOpts as any).store.staticPrefetchBailout) { + await generatePrefetchRsc( + req, + path, + res, + pathname, + htmlFilepath, + renderOpts + ) + } const { staticBailoutInfo = {} } = metadata diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 2fa85426c9f41..ede749c02a3d9 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -531,7 +531,6 @@ createNextDescribe( 'force-static/second.html', 'ssg-draft-mode/test.html', 'blog/seb/second-post.html', - 'force-static.prefetch.rsc', 'ssg-draft-mode/test-2.rsc', 'blog/styfle/first-post.rsc', 'default-cache.prefetch.rsc', From ac69094a52b5436ec96dce3e7963cf50fbd110f9 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Sun, 1 Oct 2023 15:54:08 -0700 Subject: [PATCH 09/11] remove unneeded property --- .../components/static-generation-async-storage.external.ts | 1 - .../async-storage/static-generation-async-storage-wrapper.ts | 5 ----- 2 files changed, 6 deletions(-) diff --git a/packages/next/src/client/components/static-generation-async-storage.external.ts b/packages/next/src/client/components/static-generation-async-storage.external.ts index 256e7457c1127..5caec74a52ff2 100644 --- a/packages/next/src/client/components/static-generation-async-storage.external.ts +++ b/packages/next/src/client/components/static-generation-async-storage.external.ts @@ -6,7 +6,6 @@ import { createAsyncLocalStorage } from './async-local-storage' export interface StaticGenerationStore { readonly isStaticGeneration: boolean - readonly isStaticPrefetch?: boolean readonly pagePath?: string readonly urlPathname: string readonly incrementalCache?: IncrementalCache diff --git a/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts b/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts index c01736aabbec5..4da466887163a 100644 --- a/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts +++ b/packages/next/src/server/async-storage/static-generation-async-storage-wrapper.ts @@ -2,7 +2,6 @@ import type { AsyncStorageWrapper } from './async-storage-wrapper' import type { StaticGenerationStore } from '../../client/components/static-generation-async-storage.external' import type { AsyncLocalStorage } from 'async_hooks' import type { IncrementalCache } from '../lib/incremental-cache' -import { PHASE_PRODUCTION_BUILD } from '../../shared/lib/constants' export type StaticGenerationContext = { urlPathname: string @@ -65,10 +64,6 @@ export const StaticGenerationAsyncStorageWrapper: AsyncStorageWrapper< const store: StaticGenerationStore = { isStaticGeneration, - isStaticPrefetch: Boolean( - process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD && - renderOpts.isPrefetch - ), urlPathname, pagePath: renderOpts.originalPathname, incrementalCache: From 4463bd0f0d9bcffbbfc7bd6501a1b74765ab1e41 Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Sun, 1 Oct 2023 16:05:31 -0700 Subject: [PATCH 10/11] test rename --- .../app-prefetch-static.test.ts} | 2 +- .../app/[region]/(default)/dynamic-area/[slug]/page.js | 0 .../app/[region]/(default)/layout.js | 0 .../app/[region]/(default)/static-prefetch/page.js | 0 .../{app-prefetch-i18n => app-prefetch-static}/app/layout.js | 0 .../{app-prefetch-i18n => app-prefetch-static}/app/page.js | 2 +- 6 files changed, 2 insertions(+), 2 deletions(-) rename test/e2e/app-dir/{app-prefetch-i18n/app-prefetch-i18n.test.ts => app-prefetch-static/app-prefetch-static.test.ts} (97%) rename test/e2e/app-dir/{app-prefetch-i18n => app-prefetch-static}/app/[region]/(default)/dynamic-area/[slug]/page.js (100%) rename test/e2e/app-dir/{app-prefetch-i18n => app-prefetch-static}/app/[region]/(default)/layout.js (100%) rename test/e2e/app-dir/{app-prefetch-i18n => app-prefetch-static}/app/[region]/(default)/static-prefetch/page.js (100%) rename test/e2e/app-dir/{app-prefetch-i18n => app-prefetch-static}/app/layout.js (100%) rename test/e2e/app-dir/{app-prefetch-i18n => app-prefetch-static}/app/page.js (53%) diff --git a/test/e2e/app-dir/app-prefetch-i18n/app-prefetch-i18n.test.ts b/test/e2e/app-dir/app-prefetch-static/app-prefetch-static.test.ts similarity index 97% rename from test/e2e/app-dir/app-prefetch-i18n/app-prefetch-i18n.test.ts rename to test/e2e/app-dir/app-prefetch-static/app-prefetch-static.test.ts index 9e35759f11ee9..9c7ee32c1de22 100644 --- a/test/e2e/app-dir/app-prefetch-i18n/app-prefetch-i18n.test.ts +++ b/test/e2e/app-dir/app-prefetch-static/app-prefetch-static.test.ts @@ -2,7 +2,7 @@ import { createNextDescribe } from '../../../lib/e2e-utils' import { waitFor } from '../../../lib/next-test-utils' createNextDescribe( - 'app-prefetch-i18n', + 'app-prefetch-static', { files: __dirname, }, diff --git a/test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/dynamic-area/[slug]/page.js b/test/e2e/app-dir/app-prefetch-static/app/[region]/(default)/dynamic-area/[slug]/page.js similarity index 100% rename from test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/dynamic-area/[slug]/page.js rename to test/e2e/app-dir/app-prefetch-static/app/[region]/(default)/dynamic-area/[slug]/page.js diff --git a/test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/layout.js b/test/e2e/app-dir/app-prefetch-static/app/[region]/(default)/layout.js similarity index 100% rename from test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/layout.js rename to test/e2e/app-dir/app-prefetch-static/app/[region]/(default)/layout.js diff --git a/test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/static-prefetch/page.js b/test/e2e/app-dir/app-prefetch-static/app/[region]/(default)/static-prefetch/page.js similarity index 100% rename from test/e2e/app-dir/app-prefetch-i18n/app/[region]/(default)/static-prefetch/page.js rename to test/e2e/app-dir/app-prefetch-static/app/[region]/(default)/static-prefetch/page.js diff --git a/test/e2e/app-dir/app-prefetch-i18n/app/layout.js b/test/e2e/app-dir/app-prefetch-static/app/layout.js similarity index 100% rename from test/e2e/app-dir/app-prefetch-i18n/app/layout.js rename to test/e2e/app-dir/app-prefetch-static/app/layout.js diff --git a/test/e2e/app-dir/app-prefetch-i18n/app/page.js b/test/e2e/app-dir/app-prefetch-static/app/page.js similarity index 53% rename from test/e2e/app-dir/app-prefetch-i18n/app/page.js rename to test/e2e/app-dir/app-prefetch-static/app/page.js index f35d2ac8dabcc..442d7ad2a38bd 100644 --- a/test/e2e/app-dir/app-prefetch-i18n/app/page.js +++ b/test/e2e/app-dir/app-prefetch-static/app/page.js @@ -1,3 +1,3 @@ export default function Main() { - return
Hey
+ return
Main Page
} From e11ba4da2627b98d08598bcc79a48aed8e63eded Mon Sep 17 00:00:00 2001 From: Zack Tanner Date: Mon, 2 Oct 2023 08:25:35 -0700 Subject: [PATCH 11/11] tweak test import --- .../app-prefetch-static/app-prefetch-static.test.ts | 10 +++++----- .../app/[region]/(default)/dynamic-area/[slug]/page.js | 2 +- .../app/[region]/(default)/static-prefetch/page.js | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/e2e/app-dir/app-prefetch-static/app-prefetch-static.test.ts b/test/e2e/app-dir/app-prefetch-static/app-prefetch-static.test.ts index 9c7ee32c1de22..ed24ce2f3b1e0 100644 --- a/test/e2e/app-dir/app-prefetch-static/app-prefetch-static.test.ts +++ b/test/e2e/app-dir/app-prefetch-static/app-prefetch-static.test.ts @@ -1,5 +1,5 @@ -import { createNextDescribe } from '../../../lib/e2e-utils' -import { waitFor } from '../../../lib/next-test-utils' +import { createNextDescribe } from 'e2e-utils' +import { waitFor } from 'next-test-utils' createNextDescribe( 'app-prefetch-static', @@ -20,19 +20,19 @@ createNextDescribe( await browser.elementByCss('#static-prefetch').click() expect(await browser.elementByCss('#static-prefetch-page').text()).toBe( - 'Hello from Static Page' + 'Hello from Static Prefetch Page' ) await browser.elementByCss('#dynamic-prefetch').click() expect(await browser.elementByCss('#dynamic-prefetch-page').text()).toBe( - 'Hello from Dynamic Page' + 'Hello from Dynamic Prefetch Page' ) await browser.elementByCss('#static-prefetch').click() expect(await browser.elementByCss('#static-prefetch-page').text()).toBe( - 'Hello from Static Page' + 'Hello from Static Prefetch Page' ) }) } diff --git a/test/e2e/app-dir/app-prefetch-static/app/[region]/(default)/dynamic-area/[slug]/page.js b/test/e2e/app-dir/app-prefetch-static/app/[region]/(default)/dynamic-area/[slug]/page.js index ac5e4d8294eb3..6040b75de167b 100644 --- a/test/e2e/app-dir/app-prefetch-static/app/[region]/(default)/dynamic-area/[slug]/page.js +++ b/test/e2e/app-dir/app-prefetch-static/app/[region]/(default)/dynamic-area/[slug]/page.js @@ -1,3 +1,3 @@ export default async function DynamicPage({ params, searchParams }) { - return
Hello from Dynamic Page
+ return
Hello from Dynamic Prefetch Page
} diff --git a/test/e2e/app-dir/app-prefetch-static/app/[region]/(default)/static-prefetch/page.js b/test/e2e/app-dir/app-prefetch-static/app/[region]/(default)/static-prefetch/page.js index 837a9cff1fd86..067d0371e6aef 100644 --- a/test/e2e/app-dir/app-prefetch-static/app/[region]/(default)/static-prefetch/page.js +++ b/test/e2e/app-dir/app-prefetch-static/app/[region]/(default)/static-prefetch/page.js @@ -3,7 +3,7 @@ export const dynamic = 'force-dynamic' export default async function StaticPrefetchPage({ params }) { return (
-

Hello from Static Page

+

Hello from Static Prefetch Page

) }