Repository: vercel/next.js
Pull request: Optimize segment data routes

This tweaks the segment data routes we create for dynamic routes to be derived from the `__PAGE__` segment route as we can create one route to handle the 3 segment outputs for a dynamic route. 

This also removes the segment data routes for static routes as they are not needed and be routed to without an explicit regex route per entry.  

Changed files:
filename: packages/next/errors.json
patch: @@ -756,5 +756,6 @@
   "755": "Route must be a string",
   "756": "Route %s not found",
   "757": "Unknown styled string type: %s",
-  "758": "Missing workStore in useDynamicRouteParams"
+  "758": "Missing workStore in useDynamicRouteParams",
+  "759": "Invariant: missing __PAGE__ segmentPath"
 }

filename: packages/next/src/build/index.ts
patch: @@ -191,7 +191,6 @@ import { InvariantError } from '../shared/lib/invariant-error'
 import { HTML_LIMITED_BOT_UA_RE_STRING } from '../shared/lib/router/utils/is-bot'
 import type { UseCacheTrackerKey } from './webpack/plugins/telemetry-plugin/use-cache-tracker-utils'
 import {
-  buildInversePrefetchSegmentDataRoute,
   buildPrefetchSegmentDataRoute,
   type PrefetchSegmentDataRoute,
 } from '../server/lib/router-utils/build-prefetch-segment-data-route'
@@ -3186,10 +3185,35 @@ export default async function build(
                     }
                   }
 
-                  dynamicRoute.prefetchSegmentDataRoutes ??= []
-                  for (const segmentPath of metadata.segmentPaths) {
+                  if (metadata.segmentPaths) {
+                    const pageSegmentPath = metadata.segmentPaths.find((item) =>
+                      item.endsWith('__PAGE__')
+                    )
+                    if (!pageSegmentPath) {
+                      throw new Error(`Invariant: missing __PAGE__ segmentPath`)
+                    }
+
+                    // We build a combined segment data route from the
+                    // page segment as we need to limit the number of
+                    // routes we output and they can be shared
+                    const builtSegmentDataRoute = buildPrefetchSegmentDataRoute(
+                      route.pathname,
+                      pageSegmentPath
+                    )
+
+                    builtSegmentDataRoute.source =
+                      builtSegmentDataRoute.source.replace(
+                        '/__PAGE__\\.segment\\.rsc$',
+                        `(?<segment>/__PAGE__\\.segment\\.rsc|\\.segment\\.rsc)(?:/)?$`
+                      )
+                    builtSegmentDataRoute.destination =
+                      builtSegmentDataRoute.destination.replace(
+                        '/__PAGE__.segment.rsc',
+                        '$segment'
+                      )
+                    dynamicRoute.prefetchSegmentDataRoutes ??= []
                     dynamicRoute.prefetchSegmentDataRoutes.push(
-                      buildPrefetchSegmentDataRoute(route.pathname, segmentPath)
+                      builtSegmentDataRoute
                     )
                   }
                 }
@@ -3610,36 +3634,6 @@ export default async function build(
           // remove temporary export folder
           await fs.rm(outdir, { recursive: true, force: true })
           await writeManifest(pagesManifestPath, pagesManifest)
-
-          if (config.experimental.clientSegmentCache) {
-            for (const route of [
-              ...routesManifest.staticRoutes,
-              ...routesManifest.dynamicRoutes,
-            ]) {
-              // If the segment paths aren't defined, we need to insert a
-              // reverse routing rule so that there isn't any conflicts
-              // with other dynamic routes for the prefetch segment
-              // routes. This is true for any route that is not PPR-enabled,
-              // including all routes defined by Pages Router.
-
-              // We don't need to add the prefetch segment data routes if it was
-              // added due to a page that was already generated. This would have
-              // happened if the page was static or partially static.
-              if (route.prefetchSegmentDataRoutes) {
-                continue
-              }
-
-              route.prefetchSegmentDataRoutes = [
-                buildInversePrefetchSegmentDataRoute(
-                  route.page,
-                  // We use the special segment path of `/_tree` because it's
-                  // the first one sent by the client router so it's the only
-                  // one we need to rewrite to the regular prefetch RSC route.
-                  '/_tree'
-                ),
-              ]
-            }
-          }
         })
 
         // As we may have modified the dynamicRoutes, we need to sort the

filename: packages/next/src/export/routes/app-page.ts
patch: @@ -216,8 +216,12 @@ export async function exportAppPage(
     )
 
     return {
-      // Only include the metadata if the environment has next support.
-      metadata: hasNextSupport ? meta : undefined,
+      // Filter the metadata if the environment does not have next support.
+      metadata: hasNextSupport
+        ? meta
+        : {
+            segmentPaths: meta.segmentPaths,
+          },
       hasEmptyStaticShell: Boolean(postponed) && html === '',
       hasPostponed: Boolean(postponed),
       cacheControl,

filename: test/e2e/app-dir/segment-cache/conflicting-routes/conflicting-routes.test.ts
patch: @@ -61,21 +61,11 @@ describe('conflicting routes', () => {
   it('handles conflict between App Router and Pages Router routes', async () => {
     const res = await segmentPrefetch('/new/templates', '/_tree')
 
-    // Should match the route defined at pages/new/templates/[[...slug]].js,
-    // not the one at app/new/[teamSlug]/page.tsx
-    if (isNextDeploy) {
-      // In a deployed environment the builder routes this to the .prefetch.rsc
-      // route, which doesn't exist, so it returns a 404.
-      // TODO: It'd probably be more correct if it didn't re-route to
-      // .prefetch.rsc and just routed to the normal Pages route. This would
-      // match the behavior in server mode. This only happens when the
-      // prefetch header is present, though, so the only observable effect right
-      // now is that it shows up as a 404 in the network panel. Either way, the
-      // page can't be prefetched by App Router because it's a Pages route.
-      expect(res.status).toBe(404)
-    } else {
-      expect(res.status).toBe(200)
-      expect(await res.text()).toContain('/new/templates/[[...slug]].js')
-    }
+    expect(res.status).toBe(200)
+    expect(await res.text()).toContain(
+      // when deployed we map to an empty object to signal to
+      // client router it should MPA navigate
+      isNextDeploy ? '{}' : '/new/templates/[[...slug]].js'
+    )
   })
 })

filename: test/e2e/app-dir/segment-cache/incremental-opt-in/segment-cache-incremental-opt-in.test.ts
patch: @@ -3,7 +3,7 @@ import { createRouterAct } from '../router-act'
 import { Page } from 'playwright'
 
 describe('segment cache (incremental opt in)', () => {
-  const { next, isNextDeploy, isNextDev } = nextTestSetup({
+  const { next, isNextDev } = nextTestSetup({
     files: __dirname,
   })
   if (isNextDev) {
@@ -102,11 +102,8 @@ describe('segment cache (incremental opt in)', () => {
 
   describe('multiple prefetches to same link are deduped', () => {
     it('page with PPR enabled', () => testPrefetchDeduping('/ppr-enabled'))
-    // FIXME: When deployed, the _tree prefetch request returns an empty 204.
-    ;(isNextDeploy ? it.failing : it)(
-      'page with PPR enabled, and has a dynamic param',
-      () => testPrefetchDeduping('/ppr-enabled/dynamic-param')
-    )
+    it('page with PPR enabled, and has a dynamic param', () =>
+      testPrefetchDeduping('/ppr-enabled/dynamic-param'))
     it('page with PPR disabled', () => testPrefetchDeduping('/ppr-disabled'))
     it('page with PPR disabled, and has a loading boundary', () =>
       testPrefetchDeduping('/ppr-disabled-with-loading-boundary'))