mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-09 21:07:46 +00:00
[next]: Fix RSC rewrite behavior (#10415)
- Removes some of the hacks from #10388 that were attempting to resolve an issue with RSC prefetches to `pages` routes in favor of adding rsc rewrites for all dynamic paths, and letting it fall through to a 404 if there's no match - Fixes an issue where RSC requests were matching the wrong path (filesystem rather than RSC variant) introduced in above mentioned change - Closes https://github.com/vercel/next.js/issues/54698
This commit is contained in:
5
.changeset/long-dingos-punch.md
Normal file
5
.changeset/long-dingos-punch.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@vercel/next": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix RSC rewrite behavior
|
||||||
@@ -1074,8 +1074,7 @@ export async function serverBuild({
|
|||||||
canUsePreviewMode,
|
canUsePreviewMode,
|
||||||
prerenderManifest.bypassToken || '',
|
prerenderManifest.bypassToken || '',
|
||||||
true,
|
true,
|
||||||
middleware.dynamicRouteMap,
|
middleware.dynamicRouteMap
|
||||||
inversedAppPathManifest
|
|
||||||
).then(arr =>
|
).then(arr =>
|
||||||
localizeDynamicRoutes(
|
localizeDynamicRoutes(
|
||||||
arr,
|
arr,
|
||||||
@@ -1090,6 +1089,32 @@ export async function serverBuild({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const pagesPlaceholderRscEntries: Record<string, FileBlob> = {};
|
||||||
|
|
||||||
|
if (appDir) {
|
||||||
|
// since we attempt to rewrite all paths to an .rsc variant,
|
||||||
|
// we need to create dummy rsc outputs for all pages entries
|
||||||
|
// this is so that an RSC request to a `pages` entry will match
|
||||||
|
// rather than falling back to a catchall `app` entry
|
||||||
|
// on the nextjs side, invalid RSC response payloads will correctly trigger an mpa navigation
|
||||||
|
const pagesManifest = path.join(
|
||||||
|
entryPath,
|
||||||
|
outputDirectory,
|
||||||
|
`server/pages-manifest.json`
|
||||||
|
);
|
||||||
|
|
||||||
|
const pagesData = await fs.readJSON(pagesManifest);
|
||||||
|
const pagesEntries = Object.keys(pagesData);
|
||||||
|
|
||||||
|
for (const page of pagesEntries) {
|
||||||
|
const pathName = page.startsWith('/') ? page.slice(1) : page;
|
||||||
|
pagesPlaceholderRscEntries[`${pathName}.rsc`] = new FileBlob({
|
||||||
|
data: '{}',
|
||||||
|
contentType: 'application/json',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { staticFiles, publicDirectoryFiles, staticDirectoryFiles } =
|
const { staticFiles, publicDirectoryFiles, staticDirectoryFiles } =
|
||||||
await getStaticFiles(entryPath, entryDirectory, outputDirectory);
|
await getStaticFiles(entryPath, entryDirectory, outputDirectory);
|
||||||
|
|
||||||
@@ -1249,6 +1274,7 @@ export async function serverBuild({
|
|||||||
...publicDirectoryFiles,
|
...publicDirectoryFiles,
|
||||||
...lambdas,
|
...lambdas,
|
||||||
...appRscPrefetches,
|
...appRscPrefetches,
|
||||||
|
...pagesPlaceholderRscEntries,
|
||||||
// Prerenders may override Lambdas -- this is an intentional behavior.
|
// Prerenders may override Lambdas -- this is an intentional behavior.
|
||||||
...prerenders,
|
...prerenders,
|
||||||
...staticPages,
|
...staticPages,
|
||||||
@@ -1632,72 +1658,22 @@ export async function serverBuild({
|
|||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
|
||||||
...(appDir
|
|
||||||
? [
|
|
||||||
// check routes that end in `.rsc` to see if a page with the resulting name (sans-.rsc) exists in the filesystem
|
|
||||||
// if so, we want to match that page instead. (This matters when prefetching a pages route while on an appdir route)
|
|
||||||
{
|
|
||||||
src: `^${path.posix.join('/', entryDirectory, '/(.*)\\.rsc$')}`,
|
|
||||||
dest: path.posix.join('/', entryDirectory, '/$1'),
|
|
||||||
has: [
|
|
||||||
{
|
|
||||||
type: 'header',
|
|
||||||
key: rscHeader,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
...(rscPrefetchHeader
|
|
||||||
? {
|
|
||||||
missing: [
|
|
||||||
{
|
|
||||||
type: 'header',
|
|
||||||
key: rscPrefetchHeader,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
check: true,
|
|
||||||
} as Route,
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
|
|
||||||
// These need to come before handle: miss or else they are grouped
|
// These need to come before handle: miss or else they are grouped
|
||||||
// with that routing section
|
// with that routing section
|
||||||
...afterFilesRewrites,
|
...afterFilesRewrites,
|
||||||
|
|
||||||
...(appDir
|
|
||||||
? [
|
|
||||||
// rewrite route back to `.rsc`, but skip checking fs
|
|
||||||
{
|
|
||||||
src: `^${path.posix.join(
|
|
||||||
'/',
|
|
||||||
entryDirectory,
|
|
||||||
'/((?!.+\\.rsc).+?)(?:/)?$'
|
|
||||||
)}`,
|
|
||||||
has: [
|
|
||||||
{
|
|
||||||
type: 'header',
|
|
||||||
key: rscHeader,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
dest: path.posix.join('/', entryDirectory, '/$1.rsc'),
|
|
||||||
headers: { vary: rscVaryHeader },
|
|
||||||
continue: true,
|
|
||||||
override: true,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
|
|
||||||
// make sure 404 page is used when a directory is matched without
|
|
||||||
// an index page
|
|
||||||
{ handle: 'resource' },
|
{ handle: 'resource' },
|
||||||
|
|
||||||
...fallbackRewrites,
|
...fallbackRewrites,
|
||||||
|
|
||||||
|
// make sure 404 page is used when a directory is matched without
|
||||||
|
// an index page
|
||||||
{ src: path.posix.join('/', entryDirectory, '.*'), status: 404 },
|
{ src: path.posix.join('/', entryDirectory, '.*'), status: 404 },
|
||||||
|
|
||||||
|
{ handle: 'miss' },
|
||||||
|
|
||||||
// We need to make sure to 404 for /_next after handle: miss since
|
// We need to make sure to 404 for /_next after handle: miss since
|
||||||
// handle: miss is called before rewrites and to prevent rewriting /_next
|
// handle: miss is called before rewrites and to prevent rewriting /_next
|
||||||
{ handle: 'miss' },
|
|
||||||
{
|
{
|
||||||
src: path.posix.join(
|
src: path.posix.join(
|
||||||
'/',
|
'/',
|
||||||
|
|||||||
@@ -305,8 +305,7 @@ export async function getDynamicRoutes(
|
|||||||
canUsePreviewMode?: boolean,
|
canUsePreviewMode?: boolean,
|
||||||
bypassToken?: string,
|
bypassToken?: string,
|
||||||
isServerMode?: boolean,
|
isServerMode?: boolean,
|
||||||
dynamicMiddlewareRouteMap?: Map<string, RouteWithSrc>,
|
dynamicMiddlewareRouteMap?: Map<string, RouteWithSrc>
|
||||||
appPathRoutesManifest?: Record<string, string>
|
|
||||||
): Promise<RouteWithSrc[]> {
|
): Promise<RouteWithSrc[]> {
|
||||||
if (routesManifest) {
|
if (routesManifest) {
|
||||||
switch (routesManifest.version) {
|
switch (routesManifest.version) {
|
||||||
@@ -379,17 +378,15 @@ export async function getDynamicRoutes(
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
routes.push({
|
||||||
|
...route,
|
||||||
|
src: route.src.replace(
|
||||||
|
new RegExp(escapeStringRegexp('(?:/)?$')),
|
||||||
|
'(?:\\.rsc)(?:/)?$'
|
||||||
|
),
|
||||||
|
dest: route.dest?.replace(/($|\?)/, '.rsc$1'),
|
||||||
|
});
|
||||||
|
|
||||||
if (appPathRoutesManifest?.[page]) {
|
|
||||||
routes.push({
|
|
||||||
...route,
|
|
||||||
src: route.src.replace(
|
|
||||||
new RegExp(escapeStringRegexp('(?:/)?$')),
|
|
||||||
'(?:\\.rsc)(?:/)?$'
|
|
||||||
),
|
|
||||||
dest: route.dest?.replace(/($|\?)/, '.rsc$1'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
routes.push(route);
|
routes.push(route);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Home</h1>
|
<h1>Home</h1>
|
||||||
|
<Link href="/en/about">About</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"headers": {
|
"headers": {
|
||||||
"RSC": "1"
|
"RSC": "1"
|
||||||
},
|
},
|
||||||
"mustContain": "<html"
|
"bodyMustBe": "{}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "/en/foobar",
|
"path": "/en/foobar",
|
||||||
|
|||||||
16
packages/next/test/fixtures/00-app-dir/app/gsp/[text]/page.js
vendored
Normal file
16
packages/next/test/fixtures/00-app-dir/app/gsp/[text]/page.js
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export default async function DynamicPage({ params }) {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>Dynamic page</h1>
|
||||||
|
<p>Param: {params.text}</p>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'one',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
9
packages/next/test/fixtures/00-app-dir/pages/[segmentA]/[segmentB]/[segmentC]/index.js
vendored
Normal file
9
packages/next/test/fixtures/00-app-dir/pages/[segmentA]/[segmentB]/[segmentC]/index.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export default function Page(props) {
|
||||||
|
return <div>SSRed Page</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getServerSideProps() {
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/next/test/fixtures/00-app-dir/pages/docs/categories/foo.js
vendored
Normal file
3
packages/next/test/fixtures/00-app-dir/pages/docs/categories/foo.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return 'Hello World'
|
||||||
|
}
|
||||||
@@ -47,6 +47,15 @@
|
|||||||
"mustContain": ":",
|
"mustContain": ":",
|
||||||
"mustNotContain": "<html"
|
"mustNotContain": "<html"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "/docs/categories/foo?_rsc=f7pci",
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"RSC": "1",
|
||||||
|
"Next-Router-Prefetch": "1"
|
||||||
|
},
|
||||||
|
"bodyMustBe": "{}"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "/ssg",
|
"path": "/ssg",
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -235,6 +244,24 @@
|
|||||||
},
|
},
|
||||||
"mustContain": ":{",
|
"mustContain": ":{",
|
||||||
"mustNotContain": "<html"
|
"mustNotContain": "<html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/gsp/one",
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"RSC": "1"
|
||||||
|
},
|
||||||
|
"mustContain": ":{",
|
||||||
|
"mustNotContain": "<html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "/gsp/two",
|
||||||
|
"status": 200,
|
||||||
|
"headers": {
|
||||||
|
"RSC": "1"
|
||||||
|
},
|
||||||
|
"mustContain": ":{",
|
||||||
|
"mustNotContain": "<html"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,6 +220,16 @@ async function runProbe(probe, deploymentId, deploymentUrl, ctx) {
|
|||||||
hadTest = true;
|
hadTest = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (probe.bodyMustBe) {
|
||||||
|
if (text !== probe.bodyMustBe) {
|
||||||
|
throw new Error(
|
||||||
|
`Fetched page ${probeUrl} does not have an exact body match of ${probe.bodyMustBe}. Content: ${text}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
hadTest = true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type Record<string, string[]>
|
* @type Record<string, string[]>
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user