mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-10 04:22:12 +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,
|
||||
prerenderManifest.bypassToken || '',
|
||||
true,
|
||||
middleware.dynamicRouteMap,
|
||||
inversedAppPathManifest
|
||||
middleware.dynamicRouteMap
|
||||
).then(arr =>
|
||||
localizeDynamicRoutes(
|
||||
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 } =
|
||||
await getStaticFiles(entryPath, entryDirectory, outputDirectory);
|
||||
|
||||
@@ -1249,6 +1274,7 @@ export async function serverBuild({
|
||||
...publicDirectoryFiles,
|
||||
...lambdas,
|
||||
...appRscPrefetches,
|
||||
...pagesPlaceholderRscEntries,
|
||||
// Prerenders may override Lambdas -- this is an intentional behavior.
|
||||
...prerenders,
|
||||
...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
|
||||
// with that routing section
|
||||
...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' },
|
||||
|
||||
...fallbackRewrites,
|
||||
|
||||
// make sure 404 page is used when a directory is matched without
|
||||
// an index page
|
||||
{ src: path.posix.join('/', entryDirectory, '.*'), status: 404 },
|
||||
|
||||
{ handle: 'miss' },
|
||||
|
||||
// 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' },
|
||||
{
|
||||
src: path.posix.join(
|
||||
'/',
|
||||
|
||||
@@ -305,8 +305,7 @@ export async function getDynamicRoutes(
|
||||
canUsePreviewMode?: boolean,
|
||||
bypassToken?: string,
|
||||
isServerMode?: boolean,
|
||||
dynamicMiddlewareRouteMap?: Map<string, RouteWithSrc>,
|
||||
appPathRoutesManifest?: Record<string, string>
|
||||
dynamicMiddlewareRouteMap?: Map<string, RouteWithSrc>
|
||||
): Promise<RouteWithSrc[]> {
|
||||
if (routesManifest) {
|
||||
switch (routesManifest.version) {
|
||||
@@ -379,8 +378,6 @@ export async function getDynamicRoutes(
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (appPathRoutesManifest?.[page]) {
|
||||
routes.push({
|
||||
...route,
|
||||
src: route.src.replace(
|
||||
@@ -389,7 +386,7 @@ export async function getDynamicRoutes(
|
||||
),
|
||||
dest: route.dest?.replace(/($|\?)/, '.rsc$1'),
|
||||
});
|
||||
}
|
||||
|
||||
routes.push(route);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Home</h1>
|
||||
<Link href="/en/about">About</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"headers": {
|
||||
"RSC": "1"
|
||||
},
|
||||
"mustContain": "<html"
|
||||
"bodyMustBe": "{}"
|
||||
},
|
||||
{
|
||||
"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": ":",
|
||||
"mustNotContain": "<html"
|
||||
},
|
||||
{
|
||||
"path": "/docs/categories/foo?_rsc=f7pci",
|
||||
"status": 200,
|
||||
"headers": {
|
||||
"RSC": "1",
|
||||
"Next-Router-Prefetch": "1"
|
||||
},
|
||||
"bodyMustBe": "{}"
|
||||
},
|
||||
{
|
||||
"path": "/ssg",
|
||||
"status": 200,
|
||||
@@ -235,6 +244,24 @@
|
||||
},
|
||||
"mustContain": ":{",
|
||||
"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;
|
||||
}
|
||||
|
||||
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[]>
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user