diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index a5d666ab6538f..1e1a1586088b5 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -128,6 +128,7 @@ import { flatReaddir } from '../lib/flat-readdir' import { eventSwcPlugins } from '../telemetry/events/swc-plugins' import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' import { + NEXT_ROUTER_PREFETCH, RSC, RSC_CONTENT_TYPE_HEADER, RSC_VARY_HEADER, @@ -223,6 +224,7 @@ export type RoutesManifest = { rsc: { header: typeof RSC varyHeader: typeof RSC_VARY_HEADER + prefetchHeader: typeof NEXT_ROUTER_PREFETCH } skipMiddlewareUrlNormalize?: boolean caseSensitive?: boolean @@ -782,6 +784,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, @@ -1135,6 +1138,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() @@ -1653,6 +1657,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)) { @@ -2499,6 +2511,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/worker.ts b/packages/next/src/export/worker.ts index e84262b775dd7..832774878b41b 100644 --- a/packages/next/src/export/worker.ts +++ b/packages/next/src/export/worker.ts @@ -50,6 +50,11 @@ import { signalFromNodeResponse, } from '../server/web/spec-extension/adapters/next-request' import * as ciEnvironment from '../telemetry/ci-info' +import { + NEXT_ROUTER_PREFETCH, + NEXT_URL, + RSC, +} from '../client/components/app-router-headers' const envConfig = require('../shared/lib/runtime-config') @@ -164,6 +169,7 @@ export default async function exportPage({ const { page } = pathMap const pathname = normalizeAppPath(page) const isAppDir = Boolean(pathMap._isAppDir) + const isAppPrefetch = Boolean(pathMap._isAppPrefetch) const isDynamicError = pathMap._isDynamicError const filePath = normalizePagePath(path) const isDynamic = isDynamicRoute(page) @@ -387,7 +393,45 @@ export default async function exportPage({ err.digest === NEXT_DYNAMIC_NO_SSR_CODE || isRedirectError(err) - if (isRouteHandler) { + const isNotFoundPage = page === '/_not-found' + + const generatePrefetchRsc = async () => { + // If we bail for prerendering due to dynamic usage we need to + // generate a static prefetch payload to prevent invoking + // functions during runtime just for prefetching + + const { renderToHTMLOrFlight } = + require('../server/app-render/app-render') as typeof import('../server/app-render/app-render') + req.headers[RSC.toLowerCase()] = '1' + req.headers[NEXT_URL.toLowerCase()] = path + req.headers[NEXT_ROUTER_PREFETCH.toLowerCase()] = '1' + + curRenderOpts.supportsDynamicHTML = true + delete (curRenderOpts as any).isRevalidate + + const prefetchRenderResult = await renderToHTMLOrFlight( + req as any, + res as any, + isNotFoundPage ? '/404' : pathname, + query, + curRenderOpts as any + ) + prefetchRenderResult.pipe(res as import('http').ServerResponse) + await res.hasStreamed + const prefetchRscData = Buffer.concat(res.buffers).toString() + + await promises.writeFile( + htmlFilepath.replace(/\.html$/, '.prefetch.rsc'), + prefetchRscData + ) + } + + // for dynamic routes with no generate static params + // we generate strictly the prefetch RSC payload to + // avoid attempting to render with default params e.g. [slug] + if (isAppPrefetch) { + await generatePrefetchRsc() + } else if (isRouteHandler) { // Ensure that the url for the page is absolute. req.url = `http://localhost:3000${req.url}` const request = NextRequestAdapter.fromNodeNextRequest( @@ -493,8 +537,6 @@ export default async function exportPage({ try { curRenderOpts.params ||= {} - - const isNotFoundPage = page === '/_not-found' const result = await renderToHTMLOrFlight( req as any, res as any, @@ -537,6 +579,8 @@ export default async function exportPage({ throw new Error( `Page with dynamic = "error" encountered dynamic data method on ${path}.` ) + } else { + await generatePrefetchRsc() } const staticBailoutInfo = metadata.staticBailoutInfo || {} diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index b42707e73c699..567734b490327 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -85,6 +85,8 @@ import { RSC_VARY_HEADER, FLIGHT_PARAMETERS, NEXT_RSC_UNION_QUERY, + NEXT_ROUTER_PREFETCH, + RSC_CONTENT_TYPE_HEADER, } from '../client/components/app-router-headers' import { MatchOptions, @@ -324,6 +326,10 @@ export default abstract class Server { renderOpts: RenderOpts ): Promise + protected async getPrefetchRsc(_pathname: string): Promise { + return null + } + protected abstract getIncrementalCache(options: { requestHeaders: Record requestProtocol: 'http' | 'https' @@ -1935,6 +1941,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/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index cdbc25860d143..870528c8c2040 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -309,6 +309,7 @@ export type ExportPathMap = { page: string query?: NextParsedUrlQuery _isAppDir?: boolean + _isAppPrefetch?: boolean _isDynamicError?: boolean } } diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 647ee26667545..6f3a72de94171 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -1045,6 +1045,12 @@ export default class NextNodeServer extends BaseServer { return this.runApi(req, res, query, match) } + protected async getPrefetchRsc(pathname: string) { + return this.getCacheFilesystem() + .readFile(join(this.serverDistDir, 'app', `${pathname}.prefetch.rsc`)) + .then((res) => res.toString()) + } + protected getCacheFilesystem(): CacheFs { return nodeFs } 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 2df13917935cc..a51553185df11 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -483,202 +483,221 @@ createNextDescribe( ) }) - expect(files).toEqual([ - '(new)/custom/page_client-reference-manifest.js', - '(new)/custom/page.js', - 'api/draft-mode/route.js', - 'api/revalidate-path-edge/route.js', - 'api/revalidate-path-node/route.js', - 'api/revalidate-tag-edge/route.js', - 'api/revalidate-tag-node/route.js', - 'blog/[author]/[slug]/page_client-reference-manifest.js', - 'blog/[author]/[slug]/page.js', - 'blog/[author]/page_client-reference-manifest.js', - 'blog/[author]/page.js', - 'blog/seb.html', - 'blog/seb.rsc', - 'blog/seb/second-post.html', - 'blog/seb/second-post.rsc', - 'blog/styfle.html', - 'blog/styfle.rsc', - 'blog/styfle/first-post.html', - 'blog/styfle/first-post.rsc', - 'blog/styfle/second-post.html', - 'blog/styfle/second-post.rsc', - 'blog/tim.html', - 'blog/tim.rsc', - 'blog/tim/first-post.html', - 'blog/tim/first-post.rsc', - 'default-cache/page_client-reference-manifest.js', - 'default-cache/page.js', - 'dynamic-error/[id]/page_client-reference-manifest.js', - 'dynamic-error/[id]/page.js', - 'dynamic-no-gen-params-ssr/[slug]/page_client-reference-manifest.js', - 'dynamic-no-gen-params-ssr/[slug]/page.js', - 'dynamic-no-gen-params/[slug]/page_client-reference-manifest.js', - 'dynamic-no-gen-params/[slug]/page.js', - 'fetch-no-cache/page_client-reference-manifest.js', - 'fetch-no-cache/page.js', - 'flight/[slug]/[slug2]/page_client-reference-manifest.js', - 'flight/[slug]/[slug2]/page.js', - 'force-cache.html', - 'force-cache.rsc', - 'force-cache/page_client-reference-manifest.js', - 'force-cache/page.js', - 'force-dynamic-catch-all/[slug]/[[...id]]/page_client-reference-manifest.js', - 'force-dynamic-catch-all/[slug]/[[...id]]/page.js', - 'force-dynamic-no-prerender/[id]/page_client-reference-manifest.js', - 'force-dynamic-no-prerender/[id]/page.js', - 'force-dynamic-prerender/[slug]/page_client-reference-manifest.js', - 'force-dynamic-prerender/[slug]/page.js', - 'force-no-store/page_client-reference-manifest.js', - 'force-no-store/page.js', - 'force-static/[slug]/page_client-reference-manifest.js', - 'force-static/[slug]/page.js', - 'force-static/first.html', - 'force-static/first.rsc', - 'force-static/page_client-reference-manifest.js', - 'force-static/page.js', - 'force-static/second.html', - 'force-static/second.rsc', - 'gen-params-dynamic-revalidate/[slug]/page_client-reference-manifest.js', - 'gen-params-dynamic-revalidate/[slug]/page.js', - 'gen-params-dynamic-revalidate/one.html', - 'gen-params-dynamic-revalidate/one.rsc', - 'gen-params-dynamic/[slug]/page_client-reference-manifest.js', - 'gen-params-dynamic/[slug]/page.js', - 'hooks/use-pathname/[slug]/page_client-reference-manifest.js', - 'hooks/use-pathname/[slug]/page.js', - 'hooks/use-pathname/slug.html', - 'hooks/use-pathname/slug.rsc', - 'hooks/use-search-params.html', - 'hooks/use-search-params.rsc', - 'hooks/use-search-params/force-static.html', - 'hooks/use-search-params/force-static.rsc', - 'hooks/use-search-params/force-static/page_client-reference-manifest.js', - 'hooks/use-search-params/force-static/page.js', - 'hooks/use-search-params/page_client-reference-manifest.js', - 'hooks/use-search-params/page.js', - 'hooks/use-search-params/with-suspense.html', - 'hooks/use-search-params/with-suspense.rsc', - 'hooks/use-search-params/with-suspense/page_client-reference-manifest.js', - 'hooks/use-search-params/with-suspense/page.js', - 'index.html', - 'index.rsc', - 'page_client-reference-manifest.js', - 'page.js', - 'partial-gen-params-no-additional-lang/[lang]/[slug]/page_client-reference-manifest.js', - 'partial-gen-params-no-additional-lang/[lang]/[slug]/page.js', - 'partial-gen-params-no-additional-lang/en/RAND.html', - 'partial-gen-params-no-additional-lang/en/RAND.rsc', - 'partial-gen-params-no-additional-lang/en/first.html', - 'partial-gen-params-no-additional-lang/en/first.rsc', - 'partial-gen-params-no-additional-lang/en/second.html', - 'partial-gen-params-no-additional-lang/en/second.rsc', - 'partial-gen-params-no-additional-lang/fr/RAND.html', - 'partial-gen-params-no-additional-lang/fr/RAND.rsc', - 'partial-gen-params-no-additional-lang/fr/first.html', - 'partial-gen-params-no-additional-lang/fr/first.rsc', - 'partial-gen-params-no-additional-lang/fr/second.html', - 'partial-gen-params-no-additional-lang/fr/second.rsc', - 'partial-gen-params-no-additional-slug/[lang]/[slug]/page_client-reference-manifest.js', - 'partial-gen-params-no-additional-slug/[lang]/[slug]/page.js', - 'partial-gen-params-no-additional-slug/en/RAND.html', - 'partial-gen-params-no-additional-slug/en/RAND.rsc', - 'partial-gen-params-no-additional-slug/en/first.html', - 'partial-gen-params-no-additional-slug/en/first.rsc', - 'partial-gen-params-no-additional-slug/en/second.html', - 'partial-gen-params-no-additional-slug/en/second.rsc', - 'partial-gen-params-no-additional-slug/fr/RAND.html', - 'partial-gen-params-no-additional-slug/fr/RAND.rsc', - 'partial-gen-params-no-additional-slug/fr/first.html', - 'partial-gen-params-no-additional-slug/fr/first.rsc', - 'partial-gen-params-no-additional-slug/fr/second.html', - 'partial-gen-params-no-additional-slug/fr/second.rsc', - 'partial-gen-params/[lang]/[slug]/page_client-reference-manifest.js', - 'partial-gen-params/[lang]/[slug]/page.js', - 'react-fetch-deduping-edge/page_client-reference-manifest.js', - 'react-fetch-deduping-edge/page.js', - 'react-fetch-deduping-node/page_client-reference-manifest.js', - 'react-fetch-deduping-node/page.js', - 'response-url/page_client-reference-manifest.js', - 'response-url/page.js', - 'route-handler-edge/revalidate-360/route.js', - 'route-handler/post/route.js', - 'route-handler/revalidate-360-isr/route.js', - 'route-handler/revalidate-360/route.js', - 'route-handler/static-cookies/route.js', - 'ssg-draft-mode.html', - 'ssg-draft-mode.rsc', - 'ssg-draft-mode/[[...route]]/page_client-reference-manifest.js', - 'ssg-draft-mode/[[...route]]/page.js', - 'ssg-draft-mode/test-2.html', - 'ssg-draft-mode/test-2.rsc', - 'ssg-draft-mode/test.html', - 'ssg-draft-mode/test.rsc', - 'ssr-auto/cache-no-store/page_client-reference-manifest.js', - 'ssr-auto/cache-no-store/page.js', - 'ssr-auto/fetch-revalidate-zero/page_client-reference-manifest.js', - 'ssr-auto/fetch-revalidate-zero/page.js', - 'ssr-forced/page_client-reference-manifest.js', - 'ssr-forced/page.js', - 'static-to-dynamic-error-forced/[id]/page_client-reference-manifest.js', - 'static-to-dynamic-error-forced/[id]/page.js', - 'static-to-dynamic-error/[id]/page_client-reference-manifest.js', - 'static-to-dynamic-error/[id]/page.js', - 'variable-config-revalidate/revalidate-3.html', - 'variable-config-revalidate/revalidate-3.rsc', - 'variable-config-revalidate/revalidate-3/page_client-reference-manifest.js', - 'variable-config-revalidate/revalidate-3/page.js', - 'variable-revalidate-edge/body/page_client-reference-manifest.js', - 'variable-revalidate-edge/body/page.js', - 'variable-revalidate-edge/encoding/page_client-reference-manifest.js', - 'variable-revalidate-edge/encoding/page.js', - 'variable-revalidate-edge/no-store/page_client-reference-manifest.js', - 'variable-revalidate-edge/no-store/page.js', - 'variable-revalidate-edge/post-method-request/page_client-reference-manifest.js', - 'variable-revalidate-edge/post-method-request/page.js', - 'variable-revalidate-edge/post-method/page_client-reference-manifest.js', - 'variable-revalidate-edge/post-method/page.js', - 'variable-revalidate-edge/revalidate-3/page_client-reference-manifest.js', - 'variable-revalidate-edge/revalidate-3/page.js', - 'variable-revalidate/authorization.html', - 'variable-revalidate/authorization.rsc', - 'variable-revalidate/authorization/page_client-reference-manifest.js', - 'variable-revalidate/authorization/page.js', - 'variable-revalidate/cookie.html', - 'variable-revalidate/cookie.rsc', - 'variable-revalidate/cookie/page_client-reference-manifest.js', - 'variable-revalidate/cookie/page.js', - 'variable-revalidate/encoding.html', - 'variable-revalidate/encoding.rsc', - 'variable-revalidate/encoding/page_client-reference-manifest.js', - 'variable-revalidate/encoding/page.js', - 'variable-revalidate/headers-instance.html', - 'variable-revalidate/headers-instance.rsc', - 'variable-revalidate/headers-instance/page_client-reference-manifest.js', - 'variable-revalidate/headers-instance/page.js', - 'variable-revalidate/no-store/page_client-reference-manifest.js', - 'variable-revalidate/no-store/page.js', - 'variable-revalidate/post-method-request/page_client-reference-manifest.js', - 'variable-revalidate/post-method-request/page.js', - 'variable-revalidate/post-method.html', - 'variable-revalidate/post-method.rsc', - 'variable-revalidate/post-method/page_client-reference-manifest.js', - 'variable-revalidate/post-method/page.js', - 'variable-revalidate/revalidate-3.html', - 'variable-revalidate/revalidate-3.rsc', - 'variable-revalidate/revalidate-3/page_client-reference-manifest.js', - 'variable-revalidate/revalidate-3/page.js', - 'variable-revalidate/revalidate-360-isr.html', - 'variable-revalidate/revalidate-360-isr.rsc', - 'variable-revalidate/revalidate-360-isr/page_client-reference-manifest.js', - 'variable-revalidate/revalidate-360-isr/page.js', - 'variable-revalidate/revalidate-360/page_client-reference-manifest.js', - 'variable-revalidate/revalidate-360/page.js', - 'variable-revalidate/status-code/page_client-reference-manifest.js', - 'variable-revalidate/status-code/page.js', - ]) + expect(files.sort()).toEqual( + [ + 'page.js', + 'index.rsc', + 'index.html', + 'blog/seb.rsc', + 'blog/tim.rsc', + 'blog/seb.html', + 'blog/tim.html', + 'blog/styfle.rsc', + 'force-cache.rsc', + 'blog/styfle.html', + 'force-cache.html', + 'ssg-draft-mode.rsc', + 'ssr-forced/page.js', + 'custom.prefetch.rsc', + 'force-cache/page.js', + 'ssg-draft-mode.html', + '(new)/custom/page.js', + 'force-static/page.js', + 'response-url/page.js', + 'blog/[author]/page.js', + 'default-cache/page.js', + 'fetch-no-cache/page.js', + 'force-no-store/page.js', + 'force-static/first.rsc', + 'api/draft-mode/route.js', + 'blog/tim/first-post.rsc', + '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', + 'route-handler/post/route.js', + 'blog/[author]/[slug]/page.js', + 'blog/styfle/second-post.html', + 'hooks/use-pathname/slug.html', + 'hooks/use-search-params.html', + 'flight/[slug]/[slug2]/page.js', + 'variable-revalidate/cookie.rsc', + 'hooks/use-search-params/page.js', + 'ssr-auto/cache-no-store/page.js', + 'variable-revalidate/cookie.html', + 'api/revalidate-tag-edge/route.js', + 'api/revalidate-tag-node/route.js', + 'variable-revalidate/encoding.rsc', + 'api/revalidate-path-edge/route.js', + 'api/revalidate-path-node/route.js', + 'gen-params-dynamic/[slug]/page.js', + 'hooks/use-pathname/[slug]/page.js', + 'page_client-reference-manifest.js', + 'react-fetch-deduping-edge/page.js', + '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', + 'variable-revalidate/post-method.html', + 'variable-revalidate/revalidate-3.rsc', + 'gen-params-dynamic-revalidate/one.rsc', + 'route-handler/revalidate-360/route.js', + 'route-handler/static-cookies/route.js', + 'variable-revalidate-edge/body/page.js', + 'variable-revalidate/authorization.rsc', + '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', + 'variable-revalidate/post-method/page.js', + 'variable-revalidate/status-code/page.js', + 'dynamic-no-gen-params-ssr/[slug]/page.js', + 'hooks/use-search-params/force-static.rsc', + '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', + 'variable-revalidate-edge/encoding/page.js', + 'variable-revalidate-edge/no-store/page.js', + 'variable-revalidate/authorization/page.js', + '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', + 'gen-params-dynamic-revalidate/[slug]/page.js', + 'hooks/use-search-params/force-static/page.js', + 'ssr-forced/page_client-reference-manifest.js', + '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', + '(new)/custom/page_client-reference-manifest.js', + 'force-static/page_client-reference-manifest.js', + 'response-url/page_client-reference-manifest.js', + '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', + 'partial-gen-params-no-additional-lang/en/RAND.rsc', + 'partial-gen-params-no-additional-lang/fr/RAND.rsc', + 'partial-gen-params-no-additional-slug/en/RAND.rsc', + 'partial-gen-params-no-additional-slug/fr/RAND.rsc', + 'partial-gen-params-no-additional-lang/en/RAND.html', + 'partial-gen-params-no-additional-lang/en/first.rsc', + 'partial-gen-params-no-additional-lang/fr/RAND.html', + 'partial-gen-params-no-additional-lang/fr/first.rsc', + 'partial-gen-params-no-additional-slug/en/RAND.html', + 'partial-gen-params-no-additional-slug/en/first.rsc', + 'partial-gen-params-no-additional-slug/fr/RAND.html', + 'partial-gen-params-no-additional-slug/fr/first.rsc', + 'partial-gen-params-no-additional-lang/en/first.html', + 'partial-gen-params-no-additional-lang/en/second.rsc', + 'partial-gen-params-no-additional-lang/fr/first.html', + 'partial-gen-params-no-additional-lang/fr/second.rsc', + 'partial-gen-params-no-additional-slug/en/first.html', + 'partial-gen-params-no-additional-slug/en/second.rsc', + 'partial-gen-params-no-additional-slug/fr/first.html', + 'partial-gen-params-no-additional-slug/fr/second.rsc', + 'dynamic-error/[id]/page_client-reference-manifest.js', + 'partial-gen-params-no-additional-lang/en/second.html', + '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', + 'flight/[slug]/[slug2]/page_client-reference-manifest.js', + 'hooks/use-search-params/page_client-reference-manifest.js', + 'ssr-auto/cache-no-store/page_client-reference-manifest.js', + 'gen-params-dynamic/[slug]/page_client-reference-manifest.js', + 'hooks/use-pathname/[slug]/page_client-reference-manifest.js', + 'partial-gen-params-no-additional-lang/[lang]/[slug]/page.js', + 'partial-gen-params-no-additional-slug/[lang]/[slug]/page.js', + 'react-fetch-deduping-edge/page_client-reference-manifest.js', + 'react-fetch-deduping-node/page_client-reference-manifest.js', + 'variable-revalidate/cookie/page_client-reference-manifest.js', + 'ssg-draft-mode/[[...route]]/page_client-reference-manifest.js', + 'dynamic-no-gen-params/[slug]/page_client-reference-manifest.js', + 'static-to-dynamic-error/[id]/page_client-reference-manifest.js', + 'variable-revalidate/encoding/page_client-reference-manifest.js', + 'variable-revalidate/no-store/page_client-reference-manifest.js', + 'variable-revalidate-edge/body/page_client-reference-manifest.js', + 'force-dynamic-prerender/[slug]/page_client-reference-manifest.js', + 'ssr-auto/fetch-revalidate-zero/page_client-reference-manifest.js', + 'force-dynamic-no-prerender/[id]/page_client-reference-manifest.js', + 'variable-revalidate/post-method/page_client-reference-manifest.js', + 'variable-revalidate/status-code/page_client-reference-manifest.js', + 'dynamic-no-gen-params-ssr/[slug]/page_client-reference-manifest.js', + 'partial-gen-params/[lang]/[slug]/page_client-reference-manifest.js', + 'variable-revalidate/revalidate-3/page_client-reference-manifest.js', + 'variable-revalidate-edge/encoding/page_client-reference-manifest.js', + 'variable-revalidate-edge/no-store/page_client-reference-manifest.js', + 'variable-revalidate/authorization/page_client-reference-manifest.js', + 'variable-revalidate/revalidate-360/page_client-reference-manifest.js', + 'static-to-dynamic-error-forced/[id]/page_client-reference-manifest.js', + 'gen-params-dynamic-revalidate/[slug]/page_client-reference-manifest.js', + 'hooks/use-search-params/force-static/page_client-reference-manifest.js', + 'variable-revalidate-edge/post-method/page_client-reference-manifest.js', + 'variable-revalidate/headers-instance/page_client-reference-manifest.js', + 'hooks/use-search-params/with-suspense/page_client-reference-manifest.js', + 'variable-revalidate-edge/revalidate-3/page_client-reference-manifest.js', + 'variable-revalidate/revalidate-360-isr/page_client-reference-manifest.js', + 'variable-config-revalidate/revalidate-3/page_client-reference-manifest.js', + 'variable-revalidate/post-method-request/page_client-reference-manifest.js', + 'force-dynamic-catch-all/[slug]/[[...id]]/page_client-reference-manifest.js', + 'variable-revalidate-edge/post-method-request/page_client-reference-manifest.js', + 'partial-gen-params-no-additional-lang/[lang]/[slug]/page_client-reference-manifest.js', + 'partial-gen-params-no-additional-slug/[lang]/[slug]/page_client-reference-manifest.js', + ].sort() + ) }) it('should have correct prerender-manifest entries', async () => { diff --git a/test/e2e/app-dir/app/app/dashboard/(custom)/deployments/breakdown/page.js b/test/e2e/app-dir/app/app/dashboard/(custom)/deployments/breakdown/page.js index b4cdd2011b918..4dee0abb25397 100644 --- a/test/e2e/app-dir/app/app/dashboard/(custom)/deployments/breakdown/page.js +++ b/test/e2e/app-dir/app/app/dashboard/(custom)/deployments/breakdown/page.js @@ -1,4 +1,5 @@ export default function DeploymentsBreakdownPage(props) { + console.log('rendering dashboard/(custom)/deployments/breakdown') return ( <>

hello from app/dashboard/(custom)/deployments/breakdown

diff --git a/test/e2e/app-dir/app/app/dashboard/deployments/[id]/page.js b/test/e2e/app-dir/app/app/dashboard/deployments/[id]/page.js index 363008ebc1c48..2c612c64196c3 100644 --- a/test/e2e/app-dir/app/app/dashboard/deployments/[id]/page.js +++ b/test/e2e/app-dir/app/app/dashboard/deployments/[id]/page.js @@ -16,6 +16,7 @@ async function getData({ params }) { } export default function DeploymentsPage(props) { + console.log('rendering /dashboard/deployments/[id]') const data = use(getData(props)) return ( diff --git a/test/e2e/app-dir/app/app/linking/about/page.js b/test/e2e/app-dir/app/app/linking/about/page.js index dfc1aee0840f9..4ee5555df6e7f 100644 --- a/test/e2e/app-dir/app/app/linking/about/page.js +++ b/test/e2e/app-dir/app/app/linking/about/page.js @@ -1,3 +1,4 @@ export default function AboutPage() { + console.log('rendering linking about page') return

About page

} diff --git a/test/e2e/app-dir/app/app/linking/layout.js b/test/e2e/app-dir/app/app/linking/layout.js index f836cd6b0cc41..bbbebe18e3a26 100644 --- a/test/e2e/app-dir/app/app/linking/layout.js +++ b/test/e2e/app-dir/app/app/linking/layout.js @@ -5,8 +5,18 @@ export default function Layout({ children }) { <>
{children} diff --git a/test/e2e/app-dir/app/app/linking/page.js b/test/e2e/app-dir/app/app/linking/page.js index ee48946c44f5e..c95e692a6072a 100644 --- a/test/e2e/app-dir/app/app/linking/page.js +++ b/test/e2e/app-dir/app/app/linking/page.js @@ -1,3 +1,4 @@ export default function Page() { + console.log('rendering linking home page') return

Home page

} diff --git a/test/e2e/app-dir/app/index.test.ts b/test/e2e/app-dir/app/index.test.ts index 0c5bae1ab70bb..9b4404f888462 100644 --- a/test/e2e/app-dir/app/index.test.ts +++ b/test/e2e/app-dir/app/index.test.ts @@ -3,6 +3,8 @@ import crypto from 'crypto' 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', @@ -11,6 +13,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') + ).toBeTruthy() + 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 0a28aa5f21698..18d63877cffdd 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 0f9530cce5388..202039c591672 100644 --- a/test/integration/dynamic-routing/test/index.test.js +++ b/test/integration/dynamic-routing/test/index.test.js @@ -1493,6 +1493,7 @@ function runTests({ dev }) { contentTypeHeader: 'text/x-component', varyHeader: 'RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url', + prefetchHeader: 'Next-Router-Prefetch', }, }) })