[next] Ensure all static routes have static streaming lambda path (#11259)

When there's a match for a prerender, the dynamic query parameters will
not be provided via the `x-now-route-matches`. This ensures that when
generating any static route that the static streaming lambda path is
used if it's provided, ensuring that when a page is generated via
`generateStaticParams`:

```tsx
// app/blog/[slug]/page.tsx

type Props = {
  params: {
    slug: string
  }
}

export function generateStaticParams() {
  return [{ slug: "one" }, { slug: "two" }, { slug: "three" }]
}

export default function BlogPage({ slug }: Props) {
  // ...
}
```

That we use the specific routes (`/blog/one`, `/blog/two`, and
`/blog/three`) for the partial prerendering streaming path instead of
the paramatarized pathname (`/blog/[slug]`) as the rewrites won't be
matched once a match for a prerender has been found.

This additionally updates the tests to ensure that the correct pathname
is observed (this was previously silently failing).
This commit is contained in:
Wyatt Johnson
2024-03-14 12:37:48 -06:00
committed by GitHub
parent fab5fca939
commit 4bca0c6d0b
6 changed files with 72 additions and 42 deletions

View File

@@ -0,0 +1,5 @@
---
'@vercel/next': patch
---
Ensure that static PPR pages have static streaming lambda paths.

View File

@@ -2142,7 +2142,6 @@ export const build: BuildV2 = async ({
appPathRoutesManifest,
isSharedLambdas,
canUsePreviewMode,
omittedPrerenderRoutes,
});
await Promise.all(

View File

@@ -1229,34 +1229,31 @@ export async function serverBuild({
// We want to add the `experimentalStreamingLambdaPath` to this
// output.
experimentalStreamingLambdaPaths.set(outputName, key);
} else {
// As this is an omitted page, we should generate the experimental
// partial prerendering resume route for each of these routes that
// support partial prerendering. This is because the routes that
// haven't been omitted will have rewrite rules in place to rewrite
// the original request `/blog/my-slug` to the dynamic path
// `/blog/[slug]?nxtPslug=my-slug`.
for (const [
routePathname,
{ srcRoute, experimentalPPR },
] of Object.entries(prerenderManifest.staticRoutes)) {
// If the srcRoute doesn't match or this doesn't support
// experimental partial prerendering, then we can skip this route.
if (srcRoute !== pagePathname || !experimentalPPR) continue;
}
// If this route is the same as the page route, then we can skip
// it, because we've already added the lambda to the output.
if (routePathname === pagePathname) continue;
// For each static route that was generated, we should generate a
// specific partial prerendering resume route. This is because any
// static route that is matched will not hit the rewrite rules.
for (const [
routePathname,
{ srcRoute, experimentalPPR },
] of Object.entries(prerenderManifest.staticRoutes)) {
// If the srcRoute doesn't match or this doesn't support
// experimental partial prerendering, then we can skip this route.
if (srcRoute !== pagePathname || !experimentalPPR) continue;
const key = getPostponeResumePathname(
entryDirectory,
routePathname
);
lambdas[key] = lambda;
// If this route is the same as the page route, then we can skip
// it, because we've already added the lambda to the output.
if (routePathname === pagePathname) continue;
outputName = path.posix.join(entryDirectory, routePathname);
experimentalStreamingLambdaPaths.set(outputName, key);
}
const key = getPostponeResumePathname(
entryDirectory,
routePathname
);
lambdas[key] = lambda;
outputName = path.posix.join(entryDirectory, routePathname);
experimentalStreamingLambdaPaths.set(outputName, key);
}
continue;
@@ -1314,7 +1311,6 @@ export async function serverBuild({
hasPages404: routesManifest.pages404,
isCorrectNotFoundRoutes,
isEmptyAllowQueryForPrendered,
omittedPrerenderRoutes,
});
await Promise.all(

View File

@@ -1938,7 +1938,6 @@ type OnPrerenderRouteArgs = {
routesManifest?: RoutesManifest;
isCorrectNotFoundRoutes?: boolean;
isEmptyAllowQueryForPrendered?: boolean;
omittedPrerenderRoutes: ReadonlySet<string>;
};
let prerenderGroup = 1;
@@ -1975,7 +1974,6 @@ export const onPrerenderRoute =
routesManifest,
isCorrectNotFoundRoutes,
isEmptyAllowQueryForPrendered,
omittedPrerenderRoutes,
} = prerenderRouteArgs;
if (isBlocking && isFallback) {
@@ -2211,12 +2209,19 @@ export const onPrerenderRoute =
initialStatus = 404;
}
/**
* If the route key had an `/index` suffix added, we need to note it so we
* can remove it from the output path later accurately.
*/
let addedIndexSuffix = false;
if (isAppPathRoute) {
// for literal index routes we need to append an additional /index
// due to the proxy's normalizing for /index routes
if (routeKey !== '/index' && routeKey.endsWith('/index')) {
routeKey = `${routeKey}/index`;
routeFileNoExt = routeKey;
addedIndexSuffix = true;
}
}
@@ -2225,6 +2230,7 @@ export const onPrerenderRoute =
if (!isAppPathRoute) {
outputPathPage = normalizeIndexOutput(outputPathPage, isServerMode);
}
const outputPathPageOrig = path.posix.join(
entryDirectory,
origRouteFileNoExt
@@ -2408,20 +2414,25 @@ export const onPrerenderRoute =
);
}
// If a source route exists, and it's not listed as an omitted route,
// then use the src route as the basis for the experimental streaming
// lambda path. If the route doesn't have a source route or it's not
// omitted, then use the more specific `routeKey` as the basis.
if (srcRoute && !omittedPrerenderRoutes.has(srcRoute)) {
// Try to get the experimental streaming lambda path for the specific
// static route first, then try the srcRoute if it doesn't exist. If we
// can't find it at all, this constitutes an error.
experimentalStreamingLambdaPath = experimentalStreamingLambdaPaths.get(
pathnameToOutputName(entryDirectory, routeKey, addedIndexSuffix)
);
if (!experimentalStreamingLambdaPath && srcRoute) {
experimentalStreamingLambdaPath =
experimentalStreamingLambdaPaths.get(
pathnameToOutputName(entryDirectory, srcRoute)
);
} else {
experimentalStreamingLambdaPath =
experimentalStreamingLambdaPaths.get(
pathnameToOutputName(entryDirectory, routeKey)
);
}
if (!experimentalStreamingLambdaPath) {
throw new Error(
`Invariant: experimentalStreamingLambdaPath is undefined for routeKey=${routeKey} and srcRoute=${
srcRoute ?? 'null'
}`
);
}
}
@@ -2651,8 +2662,20 @@ export function getNextServerPath(nextVersion: string) {
: 'next/dist/next-server/server';
}
export function pathnameToOutputName(entryDirectory: string, pathname: string) {
if (pathname === '/') pathname = '/index';
function pathnameToOutputName(
entryDirectory: string,
pathname: string,
addedIndexSuffix = false
) {
if (pathname === '/') {
pathname = '/index';
}
// If the `/index` was added for a route that ended in `/index` we need to
// strip the second one off before joining it with the entryDirectory.
else if (addedIndexSuffix) {
pathname = pathname.replace(/\/index$/, '');
}
return path.posix.join(entryDirectory, pathname);
}

View File

@@ -20,7 +20,11 @@ export const Dynamic = ({ pathname, fallback }) => {
{pathname && (
<>
<dt>Pathname</dt>
<dd>{pathname}</dd>
{/* We're encoding this using the following format so that even if
the HTML is sent as flight data, it will still retain the same
content, and can be inspected without having to run the
javascript. */}
<dd data-pathname={`data-pathname=${pathname}`}>{pathname}</dd>
</>
)}
{messages.map(({ name, value }) => (

View File

@@ -59,6 +59,9 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
const html = await res.text();
expect(html).toContain(expected);
expect(html).toContain('</html>');
// Validate that the loaded URL is correct.
expect(html).toContain(`data-pathname=${pathname}`);
}
);