[next] fix re-mapping logic for index prefetches (#10750)

Follow-up to https://github.com/vercel/vercel/pull/10734 -- but considers that the static prefetch associated with `/` might be inside of a dir such as `index/index.prefetch.rsc`. 

To avoid any future matching conflicts, this PR updates to prefix all static prefetches
This commit is contained in:
Zack Tanner
2023-10-24 13:51:55 -07:00
committed by GitHub
parent 57541e230d
commit fc90a3dc0b
7 changed files with 148 additions and 17 deletions

View File

@@ -0,0 +1,5 @@
---
"@vercel/next": patch
---
fix re-mapping logic for index prefetches

View File

@@ -49,6 +49,7 @@ import {
VariantsManifest,
RSC_CONTENT_TYPE,
RSC_PREFETCH_SUFFIX,
normalizePrefetches,
} from './utils';
import {
nodeFileTrace,
@@ -193,13 +194,7 @@ export async function serverBuild({
const rscContentTypeHeader =
routesManifest?.rsc?.contentTypeHeader || RSC_CONTENT_TYPE;
// index.{ext} outputs get mapped to `/` which we don't want to override
// dynamic routes that aren't pregenerated like the prefetch rsc payload
if (appRscPrefetches['index.prefetch.rsc']) {
appRscPrefetches['__index.prefetch.rsc'] =
appRscPrefetches['index.prefetch.rsc'];
delete appRscPrefetches['index.prefetch.rsc'];
}
appRscPrefetches = normalizePrefetches(appRscPrefetches);
// ensure all appRscPrefetches have a contentType since this is used by Next.js
// to determine if it's a valid response
@@ -1647,7 +1642,7 @@ export async function serverBuild({
dest: path.posix.join(
'/',
entryDirectory,
`/$1${RSC_PREFETCH_SUFFIX}`
`/__$1${RSC_PREFETCH_SUFFIX}`
),
headers: { vary: rscVaryHeader },
continue: true,
@@ -1742,7 +1737,7 @@ export async function serverBuild({
src: `^${path.posix.join(
'/',
entryDirectory,
`/(.+?)${RSC_PREFETCH_SUFFIX}(?:/)?$`
`/__(.+?)${RSC_PREFETCH_SUFFIX}(?:/)?$`
)}`,
dest: path.posix.join('/', entryDirectory, '/$1.rsc'),
has: [

View File

@@ -3120,3 +3120,16 @@ export async function getServerlessPages(params: {
return { pages, appPaths: normalizedAppPaths };
}
// to avoid any conflict with route matching/resolving, we prefix all prefetches (ie, __index.prefetch.rsc)
// this is to ensure that prefetches are never matched for things like a greedy match on `index.{ext}`
export function normalizePrefetches(prefetches: Record<string, FileFsRef>) {
const updatedPrefetches: Record<string, FileFsRef> = {};
for (const key in prefetches) {
const newKey = key.replace(/([^/]+\.prefetch\.rsc)$/, '__$1');
updatedPrefetches[newKey] = prefetches[key];
}
return updatedPrefetches;
}

View File

@@ -1,18 +1,27 @@
import Link from 'next/link';
export default function Page(props) {
return (
<div>
<p>catch-all {JSON.stringify(props.params || {})}</p>
)
<Link href="/">Link to /</Link>
<Link href="/index">Link to /index</Link>
</div>
);
}
export function generateStaticParams() {
return [
{
slug: ['']
slug: [''],
},
{
slug: ['first']
}
]
slug: ['index'],
},
{
slug: ['first'],
},
];
}
export const revalidate = 0
export const revalidate = 0;

View File

@@ -0,0 +1,27 @@
import Link from 'next/link';
export default function Page(props) {
return (
<div>
<p>catch-all {JSON.stringify(props.params || {})}</p>
<Link href="/nested">Link to /</Link>
<Link href="/nested/index">Link to /index</Link>
</div>
);
}
export function generateStaticParams() {
return [
{
slug: [''],
},
{
slug: ['index'],
},
{
slug: ['first'],
},
];
}
export const revalidate = 0;

View File

@@ -25,6 +25,63 @@
"mustNotContain": "<html",
"mustContain": "catch-all"
},
{
"path": "/index",
"status": 200,
"mustContain": "html"
},
{
"path": "/index",
"status": 200,
"mustContain": "catch-all"
},
{
"path": "/index",
"status": 200,
"headers": {
"RSC": 1
},
"mustNotContain": "<html",
"mustContain": "catch-all"
},
{
"path": "/nested",
"status": 200,
"mustContain": "html"
},
{
"path": "/nested",
"status": 200,
"mustContain": "catch-all"
},
{
"path": "/nested",
"status": 200,
"headers": {
"RSC": 1
},
"mustNotContain": "<html",
"mustContain": "catch-all"
},
{
"path": "/nested/index",
"status": 200,
"mustContain": "html"
},
{
"path": "/nested/index",
"status": 200,
"mustContain": "catch-all"
},
{
"path": "/nested/index",
"status": 200,
"headers": {
"RSC": 1
},
"mustNotContain": "<html",
"mustContain": "catch-all"
},
{
"path": "/first",
"status": 200,

View File

@@ -7,8 +7,9 @@ import {
getImagesConfig,
getNextConfig,
getServerlessPages,
normalizePrefetches,
} from '../../src/utils';
import { FileRef } from '@vercel/build-utils';
import { FileFsRef, FileRef } from '@vercel/build-utils';
import { genDir } from '../utils';
describe('getNextConfig', () => {
@@ -408,3 +409,27 @@ describe('getServerlessPages', () => {
expect(Object.keys(appPaths)).toEqual(['favicon.ico.js', 'index.js']);
});
});
describe('normalizePrefetches', () => {
it('should properly prefix prefetches with `__`', async () => {
const dummyFile = new FileFsRef({ fsPath: __dirname });
const appRscPrefetches = {
'index.prefetch.rsc': dummyFile,
'index/index.prefetch.rsc': dummyFile,
'foo.prefetch.rsc': dummyFile,
'foo/index.prefetch.rsc': dummyFile,
'foo/bar/baz.prefetch.rsc': dummyFile,
};
const updatedPrefetches = normalizePrefetches(appRscPrefetches);
expect(Object.keys(updatedPrefetches)).toEqual([
'__index.prefetch.rsc',
'index/__index.prefetch.rsc',
'__foo.prefetch.rsc',
'foo/__index.prefetch.rsc',
'foo/bar/__baz.prefetch.rsc',
]);
});
});