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

View File

@@ -3120,3 +3120,16 @@ export async function getServerlessPages(params: {
return { pages, appPaths: normalizedAppPaths }; 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) { export default function Page(props) {
return ( return (
<p>catch-all {JSON.stringify(props.params || {})}</p> <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() { export function generateStaticParams() {
return [ 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", "mustNotContain": "<html",
"mustContain": "catch-all" "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", "path": "/first",
"status": 200, "status": 200,

View File

@@ -7,8 +7,9 @@ import {
getImagesConfig, getImagesConfig,
getNextConfig, getNextConfig,
getServerlessPages, getServerlessPages,
normalizePrefetches,
} from '../../src/utils'; } from '../../src/utils';
import { FileRef } from '@vercel/build-utils'; import { FileFsRef, FileRef } from '@vercel/build-utils';
import { genDir } from '../utils'; import { genDir } from '../utils';
describe('getNextConfig', () => { describe('getNextConfig', () => {
@@ -408,3 +409,27 @@ describe('getServerlessPages', () => {
expect(Object.keys(appPaths)).toEqual(['favicon.ico.js', 'index.js']); 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',
]);
});
});