Repository: vercel/next.js
Pull request: Deployment adapter: fix metadata for "/" route

Updates cases where `/` isn't normalized fully when interpolating into data paths and adds regression tests. 

Changed files:
filename: packages/next/src/build/adapter/build-complete.ts
patch: @@ -727,8 +727,7 @@ export async function handleBuildComplete({
 
         // need to add matching .rsc output
         if (isAppPage) {
-          const rscPathname =
-            (output.pathname === '/' ? '/index' : output.pathname) + '.rsc'
+          const rscPathname = normalizePagePath(output.pathname) + '.rsc'
           outputs.appPages.push({
             ...output,
             pathname: rscPathname,
@@ -864,7 +863,7 @@ export async function handleBuildComplete({
             const dataPathname = path.posix.join(
               '/_next/data',
               buildId,
-              page + '.json'
+              normalizePagePath(page) + '.json'
             )
             outputs.pages.push({
               ...output,
@@ -1002,9 +1001,8 @@ export async function handleBuildComplete({
           if (output.type === AdapterOutputType.APP_PAGE) {
             outputs.appPages.push({
               ...output,
-              pathname:
-                (output.pathname === '/' ? '/index' : output.pathname) + '.rsc',
-              id: (output.id === '/' ? '/index' : output.pathname) + '.rsc',
+              pathname: normalizePagePath(output.pathname) + '.rsc',
+              id: normalizePagePath(output.pathname) + '.rsc',
             })
             outputs.appPages.push(output)
           } else {
@@ -1058,15 +1056,18 @@ export async function handleBuildComplete({
         }
 
         if (meta?.segmentPaths) {
+          const normalizedRoute = normalizePagePath(route)
           const segmentsDir = path.join(
             appDistDir,
-            `${route}${prefetchSegmentDirSuffix}`
+            `${normalizedRoute}${prefetchSegmentDirSuffix}`
           )
 
           for (const segmentPath of meta.segmentPaths) {
             const outputSegmentPath =
-              path.join(route + prefetchSegmentDirSuffix, segmentPath) +
-              prefetchSegmentSuffix
+              path.join(
+                normalizedRoute + prefetchSegmentDirSuffix,
+                segmentPath
+              ) + prefetchSegmentSuffix
 
             const fallbackPathname = path.join(
               segmentsDir,
@@ -1114,10 +1115,11 @@ export async function handleBuildComplete({
         route: string,
         isAppPage: boolean
       ): Promise<AppRouteMeta> => {
+        const basename = route.endsWith('/') ? `${route}index` : route
         const meta: AppRouteMeta = isAppPage
           ? JSON.parse(
               await fs
-                .readFile(path.join(appDistDir, `${route}.meta`), 'utf8')
+                .readFile(path.join(appDistDir, `${basename}.meta`), 'utf8')
                 .catch(() => '{}')
             )
           : {}
@@ -1192,7 +1194,7 @@ export async function handleBuildComplete({
 
         let filePath = path.join(
           isAppPage ? appDistDir : pagesDistDir,
-          `${route === '/' ? 'index' : route}.${isAppPage && !dataRoute ? 'body' : 'html'}`
+          `${normalizePagePath(route)}.${isAppPage && !dataRoute ? 'body' : 'html'}`
         )
 
         // we use the static 404 for notFound: true if available
@@ -1268,7 +1270,7 @@ export async function handleBuildComplete({
         if (dataRoute) {
           let dataFilePath = path.join(
             pagesDistDir,
-            `${route === '/' ? 'index' : route}.json`
+            `${normalizePagePath(route)}.json`
           )
 
           if (isAppPage) {
@@ -1593,16 +1595,18 @@ export async function handleBuildComplete({
           prefixRouteKeys: true,
           includeSuffix: true,
         })
+        const isDataRoute = dataRoutePages.has(page)
+
         const destination = path.posix.join(
           '/',
           config.basePath,
-          ...(dataRoutePages.has(page) ? [`_next/data`, buildId] : ''),
+          ...(isDataRoute ? [`_next/data`, buildId] : ''),
           ...(page === '/'
             ? [shouldLocalize ? '$nextLocale.json' : 'index.json']
             : [
                 shouldLocalize ? '$nextLocale' : '',
                 page +
-                  '.json' +
+                  (isDataRoute ? '.json' : '') +
                   getDestinationQuery(routeRegex.routeKeys || {}),
               ])
         )

filename: test/production/adapter-config/adapter-config.test.ts
patch: @@ -99,6 +99,8 @@ describe('adapter-config', () => {
       } else {
         expect(output.pathname).toStartWith('/docs/_next/static')
       }
+      // ensure / -> /index normalizing is correct
+      expect(output.pathname.includes('/.')).toBe(false)
 
       const stats = await fs.promises.stat(output.filePath)
       expect(stats.isFile()).toBe(true)
@@ -118,18 +120,35 @@ describe('adapter-config', () => {
         expect(typeof prerenderOutput.config.bypassToken).toBe('string')
         expect(Array.isArray(prerenderOutput.config.allowHeader)).toBe(true)
         expect(Array.isArray(prerenderOutput.config.allowQuery)).toBe(true)
+        // ensure / -> /index normalizing is correct
+        expect(prerenderOutput.pathname.includes('/.')).toBe(false)
       } catch (err) {
         require('console').error(`invalid prerender ${prerenderOutput.id}`, err)
         throw err
       }
     }
 
+    const indexPrerender = prerenderOutputs.find(
+      (item) => item.pathname === '/docs'
+    )
+
+    expect(indexPrerender?.fallback?.initialHeaders).toEqual({
+      'content-type': 'text/html; charset=utf-8',
+      vary: 'rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch',
+      'x-next-cache-tags': '_N_T_/layout,_N_T_/page,_N_T_/,_N_T_/index',
+      'x-nextjs-prerender': '1',
+      'x-nextjs-stale-time': '300',
+    })
+    expect(indexPrerender?.fallback?.initialRevalidate).toBe(false)
+
     for (const route of nodeOutputs) {
       try {
         expect(route.id).toBeString()
         expect(route.config).toBeObject()
         expect(route.pathname).toBeString()
         expect(route.runtime).toBe('nodejs')
+        // ensure / -> /index normalizing is correct
+        expect(route.pathname.includes('/.')).toBe(false)
 
         const stats = await fs.promises.stat(route.filePath)
         expect(stats.isFile()).toBe(true)
@@ -154,6 +173,8 @@ describe('adapter-config', () => {
         expect(route.id).toBeString()
         expect(route.config).toBeObject()
         expect(route.pathname).toBeString()
+        // ensure / -> /index normalizing is correct
+        expect(route.pathname.includes('/.')).toBe(false)
         expect(route.runtime).toBe('edge')
         expect(route.config.env).toEqual(
           expect.objectContaining({