[next] Support pre-generated pages without fallbacks with Partial Prerendering (#11183)

This commit is contained in:
Wyatt Johnson
2024-03-01 18:29:31 -07:00
committed by GitHub
parent 24ec5c5aca
commit b1d8b83abb
8 changed files with 327 additions and 108 deletions

View File

@@ -0,0 +1,5 @@
---
'@vercel/next': patch
---
Enable partial prerendering support for pre-generated pages

View File

@@ -1142,6 +1142,10 @@ export const build: BuildV2 = async ({
appPathRoutesManifest, appPathRoutesManifest,
}); });
/**
* This is a detection for preview mode that's required for the pages
* router.
*/
const canUsePreviewMode = Object.keys(pages).some(page => const canUsePreviewMode = Object.keys(pages).some(page =>
isApiPage(pages[page].fsPath) isApiPage(pages[page].fsPath)
); );
@@ -1316,6 +1320,22 @@ export const build: BuildV2 = async ({
} }
} }
/**
* All of the routes that have `experimentalPPR` enabled.
*/
const experimentalPPRRoutes = new Set<string>();
for (const [route, { experimentalPPR }] of [
...Object.entries(prerenderManifest.staticRoutes),
...Object.entries(prerenderManifest.blockingFallbackRoutes),
...Object.entries(prerenderManifest.fallbackRoutes),
...Object.entries(prerenderManifest.omittedRoutes),
]) {
if (!experimentalPPR) continue;
experimentalPPRRoutes.add(route);
}
if (requiredServerFilesManifest) { if (requiredServerFilesManifest) {
if (!routesManifest) { if (!routesManifest) {
throw new Error( throw new Error(
@@ -1371,6 +1391,7 @@ export const build: BuildV2 = async ({
hasIsr404Page, hasIsr404Page,
hasIsr500Page, hasIsr500Page,
variantsManifest, variantsManifest,
experimentalPPRRoutes,
}); });
} }
@@ -1883,17 +1904,18 @@ export const build: BuildV2 = async ({
); );
} }
dynamicRoutes = await getDynamicRoutes( dynamicRoutes = await getDynamicRoutes({
entryPath, entryPath,
entryDirectory, entryDirectory,
dynamicPages, dynamicPages,
false, isDev: false,
routesManifest, routesManifest,
omittedPrerenderRoutes, omittedRoutes: omittedPrerenderRoutes,
canUsePreviewMode, canUsePreviewMode,
prerenderManifest.bypassToken || '', bypassToken: prerenderManifest.bypassToken || '',
isServerMode isServerMode,
).then(arr => experimentalPPRRoutes,
}).then(arr =>
localizeDynamicRoutes( localizeDynamicRoutes(
arr, arr,
dynamicPrefix, dynamicPrefix,
@@ -1912,17 +1934,18 @@ export const build: BuildV2 = async ({
// we need to include the prerenderManifest.omittedRoutes here // we need to include the prerenderManifest.omittedRoutes here
// for the page to be able to be matched in the lambda for preview mode // for the page to be able to be matched in the lambda for preview mode
const completeDynamicRoutes = await getDynamicRoutes( const completeDynamicRoutes = await getDynamicRoutes({
entryPath, entryPath,
entryDirectory, entryDirectory,
dynamicPages, dynamicPages,
false, isDev: false,
routesManifest, routesManifest,
undefined, omittedRoutes: undefined,
canUsePreviewMode, canUsePreviewMode,
prerenderManifest.bypassToken || '', bypassToken: prerenderManifest.bypassToken || '',
isServerMode isServerMode,
).then(arr => experimentalPPRRoutes,
}).then(arr =>
arr.map(route => { arr.map(route => {
route.src = route.src.replace('^', `^${dynamicPrefix}`); route.src = route.src.replace('^', `^${dynamicPrefix}`);
return route; return route;
@@ -2119,22 +2142,33 @@ export const build: BuildV2 = async ({
appPathRoutesManifest, appPathRoutesManifest,
isSharedLambdas, isSharedLambdas,
canUsePreviewMode, canUsePreviewMode,
omittedPrerenderRoutes,
}); });
Object.keys(prerenderManifest.staticRoutes).forEach(route => await Promise.all(
prerenderRoute(route, { isBlocking: false, isFallback: false }) Object.keys(prerenderManifest.staticRoutes).map(route =>
prerenderRoute(route, {})
)
); );
Object.keys(prerenderManifest.fallbackRoutes).forEach(route =>
prerenderRoute(route, { isBlocking: false, isFallback: true }) await Promise.all(
Object.keys(prerenderManifest.fallbackRoutes).map(route =>
prerenderRoute(route, { isFallback: true })
)
); );
Object.keys(prerenderManifest.blockingFallbackRoutes).forEach(route =>
prerenderRoute(route, { isBlocking: true, isFallback: false }) await Promise.all(
Object.keys(prerenderManifest.blockingFallbackRoutes).map(route =>
prerenderRoute(route, { isBlocking: true })
)
); );
if (static404Page && canUsePreviewMode) { if (static404Page && canUsePreviewMode) {
omittedPrerenderRoutes.forEach(route => { await Promise.all(
prerenderRoute(route, { isOmitted: true }); Array.from(omittedPrerenderRoutes).map(route =>
}); prerenderRoute(route, { isOmitted: true })
)
);
} }
// We still need to use lazyRoutes if the dataRoutes field // We still need to use lazyRoutes if the dataRoutes field

View File

@@ -52,6 +52,7 @@ import {
RSC_PREFETCH_SUFFIX, RSC_PREFETCH_SUFFIX,
normalizePrefetches, normalizePrefetches,
CreateLambdaFromPseudoLayersOptions, CreateLambdaFromPseudoLayersOptions,
getPostponeResumePathname,
} from './utils'; } from './utils';
import { import {
nodeFileTrace, nodeFileTrace,
@@ -142,6 +143,7 @@ export async function serverBuild({
lambdaCompressedByteLimit, lambdaCompressedByteLimit,
requiredServerFilesManifest, requiredServerFilesManifest,
variantsManifest, variantsManifest,
experimentalPPRRoutes,
}: { }: {
appPathRoutesManifest?: Record<string, string>; appPathRoutesManifest?: Record<string, string>;
dynamicPages: string[]; dynamicPages: string[];
@@ -183,6 +185,7 @@ export async function serverBuild({
prerenderManifest: NextPrerenderedRoutes; prerenderManifest: NextPrerenderedRoutes;
requiredServerFilesManifest: NextRequiredServerFilesManifest; requiredServerFilesManifest: NextRequiredServerFilesManifest;
variantsManifest: VariantsManifestLegacy | null; variantsManifest: VariantsManifestLegacy | null;
experimentalPPRRoutes: ReadonlySet<string>;
}): Promise<BuildResult> { }): Promise<BuildResult> {
lambdaPages = Object.assign({}, lambdaPages, lambdaAppPaths); lambdaPages = Object.assign({}, lambdaPages, lambdaAppPaths);
@@ -353,18 +356,6 @@ export async function serverBuild({
internalPages.push('404.js'); internalPages.push('404.js');
} }
const experimentalPPRRoutes = new Set<string>();
for (const [route, { experimentalPPR }] of [
...Object.entries(prerenderManifest.staticRoutes),
...Object.entries(prerenderManifest.blockingFallbackRoutes),
...Object.entries(prerenderManifest.fallbackRoutes),
]) {
if (!experimentalPPR) continue;
experimentalPPRRoutes.add(route);
}
const prerenderRoutes: ReadonlySet<string> = new Set<string>([ const prerenderRoutes: ReadonlySet<string> = new Set<string>([
...(canUsePreviewMode ? omittedPrerenderRoutes : []), ...(canUsePreviewMode ? omittedPrerenderRoutes : []),
...Object.keys(prerenderManifest.blockingFallbackRoutes), ...Object.keys(prerenderManifest.blockingFallbackRoutes),
@@ -1185,7 +1176,7 @@ export async function serverBuild({
// lambda for the page for revalidation. // lambda for the page for revalidation.
let revalidate: NodejsLambda | undefined; let revalidate: NodejsLambda | undefined;
if (isPPR) { if (isPPR) {
if (isPPR && !options.isStreaming) { if (!options.isStreaming) {
throw new Error("Invariant: PPR lambda isn't streaming"); throw new Error("Invariant: PPR lambda isn't streaming");
} }
@@ -1197,24 +1188,28 @@ export async function serverBuild({
}); });
} }
for (const page of group.pages) { for (const pageFilename of group.pages) {
const pageNoExt = page.replace(/\.js$/, ''); // This is the name of the page, where the root is `index`.
let isPrerender = prerenderRoutes.has( const pageName = pageFilename.replace(/\.js$/, '');
path.join('/', pageNoExt === 'index' ? '' : pageNoExt)
); // This is the name of the page prefixed with a `/`, where the root is
// `/index`.
const pagePath = path.posix.join('/', pageName);
// This is the routable pathname for the page, where the root is `/`.
const pagePathname = pagePath === '/index' ? '/' : pagePath;
let isPrerender = prerenderRoutes.has(pagePathname);
if (!isPrerender && routesManifest?.i18n) { if (!isPrerender && routesManifest?.i18n) {
isPrerender = routesManifest.i18n.locales.some(locale => { isPrerender = routesManifest.i18n.locales.some(locale => {
return prerenderRoutes.has( return prerenderRoutes.has(
path.join('/', locale, pageNoExt === 'index' ? '' : pageNoExt) path.join('/', locale, pageName === 'index' ? '' : pageName)
); );
}); });
} }
let outputName = path.posix.join(entryDirectory, pageNoExt);
if (!group.isAppRouter && !group.isAppRouteHandler) { let outputName = path.posix.join(entryDirectory, pageName);
outputName = normalizeIndexOutput(outputName, true);
}
// If this is a PPR page, then we should prefix the output name. // If this is a PPR page, then we should prefix the output name.
if (isPPR) { if (isPPR) {
@@ -1222,24 +1217,56 @@ export async function serverBuild({
throw new Error("Invariant: PPR lambda isn't set"); throw new Error("Invariant: PPR lambda isn't set");
} }
// Get the get the base path prefixed route, without the index // Assign the revalidate lambda to the output name. That's used to
// normalization. // perform the initial static shell render.
outputName = path.posix.join(entryDirectory, pageNoExt);
lambdas[outputName] = revalidate; lambdas[outputName] = revalidate;
const pprOutputName = path.posix.join( // If this isn't an omitted page, then we should add the link from the
entryDirectory, // page to the postpone resume lambda.
'/_next/postponed/resume', if (!omittedPrerenderRoutes.has(pagePathname)) {
pageNoExt const key = getPostponeResumePathname(entryDirectory, pageName);
); lambdas[key] = lambda;
lambdas[pprOutputName] = lambda;
// 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;
const key = getPostponeResumePathname(
entryDirectory,
routePathname
);
lambdas[key] = lambda;
outputName = path.posix.join(entryDirectory, routePathname);
experimentalStreamingLambdaPaths.set(outputName, key);
}
}
// We want to add the `experimentalStreamingLambdaPath` to this
// output.
experimentalStreamingLambdaPaths.set(outputName, pprOutputName);
continue; continue;
} }
if (!group.isAppRouter && !group.isAppRouteHandler) {
outputName = normalizeIndexOutput(outputName, true);
}
// we add locale prefixed outputs for SSR pages, // we add locale prefixed outputs for SSR pages,
// this is handled in onPrerenderRoute for SSG pages // this is handled in onPrerenderRoute for SSG pages
if ( if (
@@ -1247,7 +1274,7 @@ export async function serverBuild({
!isPrerender && !isPrerender &&
!group.isAppRouter && !group.isAppRouter &&
(!isCorrectLocaleAPIRoutes || (!isCorrectLocaleAPIRoutes ||
!(pageNoExt === 'api' || pageNoExt.startsWith('api/'))) !(pageName === 'api' || pageName.startsWith('api/')))
) { ) {
for (const locale of i18n.locales) { for (const locale of i18n.locales) {
lambdas[ lambdas[
@@ -1255,7 +1282,7 @@ export async function serverBuild({
path.posix.join( path.posix.join(
entryDirectory, entryDirectory,
locale, locale,
pageNoExt === 'index' ? '' : pageNoExt pageName === 'index' ? '' : pageName
), ),
true true
) )
@@ -1288,6 +1315,7 @@ export async function serverBuild({
hasPages404: routesManifest.pages404, hasPages404: routesManifest.pages404,
isCorrectNotFoundRoutes, isCorrectNotFoundRoutes,
isEmptyAllowQueryForPrendered, isEmptyAllowQueryForPrendered,
omittedPrerenderRoutes,
}); });
await Promise.all( await Promise.all(
@@ -1295,11 +1323,13 @@ export async function serverBuild({
prerenderRoute(route, {}) prerenderRoute(route, {})
) )
); );
await Promise.all( await Promise.all(
Object.keys(prerenderManifest.fallbackRoutes).map(route => Object.keys(prerenderManifest.fallbackRoutes).map(route =>
prerenderRoute(route, { isFallback: true }) prerenderRoute(route, { isFallback: true })
) )
); );
await Promise.all( await Promise.all(
Object.keys(prerenderManifest.blockingFallbackRoutes).map(route => Object.keys(prerenderManifest.blockingFallbackRoutes).map(route =>
prerenderRoute(route, { isBlocking: true }) prerenderRoute(route, { isBlocking: true })
@@ -1308,9 +1338,9 @@ export async function serverBuild({
if (static404Page && canUsePreviewMode) { if (static404Page && canUsePreviewMode) {
await Promise.all( await Promise.all(
[...omittedPrerenderRoutes].map(route => { Array.from(omittedPrerenderRoutes).map(route =>
return prerenderRoute(route, { isOmitted: true }); prerenderRoute(route, { isOmitted: true })
}) )
); );
} }
@@ -1319,6 +1349,7 @@ export async function serverBuild({
if (routesManifest?.i18n) { if (routesManifest?.i18n) {
route = normalizeLocalePath(route, routesManifest.i18n.locales).pathname; route = normalizeLocalePath(route, routesManifest.i18n.locales).pathname;
} }
delete lambdas[ delete lambdas[
normalizeIndexOutput( normalizeIndexOutput(
path.posix.join('./', entryDirectory, route === '/' ? '/index' : route), path.posix.join('./', entryDirectory, route === '/' ? '/index' : route),
@@ -1342,19 +1373,19 @@ export async function serverBuild({
middleware.staticRoutes.length > 0 && middleware.staticRoutes.length > 0 &&
semver.gte(nextVersion, NEXT_DATA_MIDDLEWARE_RESOLVING_VERSION); semver.gte(nextVersion, NEXT_DATA_MIDDLEWARE_RESOLVING_VERSION);
const dynamicRoutes = await getDynamicRoutes( const dynamicRoutes = await getDynamicRoutes({
entryPath, entryPath,
entryDirectory, entryDirectory,
dynamicPages, dynamicPages,
false, isDev: false,
routesManifest, routesManifest,
omittedPrerenderRoutes, omittedRoutes: omittedPrerenderRoutes,
canUsePreviewMode, canUsePreviewMode,
prerenderManifest.bypassToken || '', bypassToken: prerenderManifest.bypassToken || '',
true, isServerMode: true,
middleware.dynamicRouteMap, dynamicMiddlewareRouteMap: middleware.dynamicRouteMap,
experimental.ppr experimentalPPRRoutes,
).then(arr => }).then(arr =>
localizeDynamicRoutes( localizeDynamicRoutes(
arr, arr,
dynamicPrefix, dynamicPrefix,
@@ -1560,6 +1591,46 @@ export async function serverBuild({
throw new Error("Invariant: cannot use PPR without 'rsc.prefetchHeader'"); throw new Error("Invariant: cannot use PPR without 'rsc.prefetchHeader'");
} }
// If we're using the Experimental Partial Prerendering, we should ensure that
// all the routes that support it (and are listed) have configured lambdas.
// This only applies to routes that do not have fallbacks enabled (these are
// routes that have `dynamicParams = false` defined.
if (experimental.ppr) {
for (const { srcRoute, dataRoute, experimentalPPR } of Object.values(
prerenderManifest.staticRoutes
)) {
// Only apply this to the routes that support experimental PPR and
// that also have their `dataRoute` and `srcRoute` defined.
if (!experimentalPPR || !dataRoute || !srcRoute) continue;
// If the srcRoute is not omitted, then we don't need to do anything. This
// is the indicator that a route should only have it's prerender defined
// and not a lambda.
if (!omittedPrerenderRoutes.has(srcRoute)) continue;
// The lambda paths have their leading `/` stripped.
const srcPathname = srcRoute.substring(1);
const dataPathname = dataRoute.substring(1);
// If we already have an associated lambda for the `.rsc` route, then
// we can skip this.
const dataPathnameExists = dataPathname in lambdas;
if (dataPathnameExists) continue;
// We require that the source route has a lambda associated with it. If
// it doesn't this is an error.
const srcPathnameExists = srcPathname in lambdas;
if (!srcPathnameExists) {
throw new Error(
`Invariant: Expected to have a lambda for the source route: ${srcPathname}`
);
}
// Associate the data pathname with the source pathname's lambda.
lambdas[dataPathname] = lambdas[srcPathname];
}
}
return { return {
wildcard: wildcardConfig, wildcard: wildcardConfig,
images: getImagesConfig(imagesManifest), images: getImagesConfig(imagesManifest),

View File

@@ -304,19 +304,31 @@ export async function getRoutesManifest(
return routesManifest; return routesManifest;
} }
export async function getDynamicRoutes( export async function getDynamicRoutes({
entryPath: string, entryPath,
entryDirectory: string, entryDirectory,
dynamicPages: ReadonlyArray<string>, dynamicPages,
isDev?: boolean, isDev,
routesManifest?: RoutesManifest, routesManifest,
omittedRoutes?: ReadonlySet<string>, omittedRoutes,
canUsePreviewMode?: boolean, canUsePreviewMode,
bypassToken?: string, bypassToken,
isServerMode?: boolean, isServerMode,
dynamicMiddlewareRouteMap?: ReadonlyMap<string, RouteWithSrc>, dynamicMiddlewareRouteMap,
experimentalPPR?: boolean experimentalPPRRoutes,
): Promise<RouteWithSrc[]> { }: {
entryPath: string;
entryDirectory: string;
dynamicPages: string[];
isDev?: boolean;
routesManifest?: RoutesManifest;
omittedRoutes?: ReadonlySet<string>;
canUsePreviewMode?: boolean;
bypassToken?: string;
isServerMode?: boolean;
dynamicMiddlewareRouteMap?: ReadonlyMap<string, RouteWithSrc>;
experimentalPPRRoutes: ReadonlySet<string>;
}): Promise<RouteWithSrc[]> {
if (routesManifest) { if (routesManifest) {
switch (routesManifest.version) { switch (routesManifest.version) {
case 1: case 1:
@@ -389,7 +401,7 @@ export async function getDynamicRoutes(
]; ];
} }
if (experimentalPPR) { if (experimentalPPRRoutes.has(page)) {
let dest = route.dest?.replace(/($|\?)/, '.prefetch.rsc$1'); let dest = route.dest?.replace(/($|\?)/, '.prefetch.rsc$1');
if (page === '/' || page === '/index') { if (page === '/' || page === '/index') {
@@ -919,6 +931,10 @@ export type NextPrerenderedRoutes = {
}; };
}; };
/**
* Routes that have their fallback behavior is disabled. All routes would've
* been provided in the top-level `routes` key (`staticRoutes`).
*/
omittedRoutes: { omittedRoutes: {
[route: string]: { [route: string]: {
routeRegex: string; routeRegex: string;
@@ -1298,8 +1314,6 @@ export async function getPrerenderManifest(
prefetchDataRouteRegex, prefetchDataRouteRegex,
}; };
} else { } else {
// Fallback behavior is disabled, all routes would've been provided
// in the top-level `routes` key (`staticRoutes`).
ret.omittedRoutes[lazyRoute] = { ret.omittedRoutes[lazyRoute] = {
experimentalBypassFor, experimentalBypassFor,
experimentalPPR, experimentalPPR,
@@ -1923,6 +1937,7 @@ type OnPrerenderRouteArgs = {
routesManifest?: RoutesManifest; routesManifest?: RoutesManifest;
isCorrectNotFoundRoutes?: boolean; isCorrectNotFoundRoutes?: boolean;
isEmptyAllowQueryForPrendered?: boolean; isEmptyAllowQueryForPrendered?: boolean;
omittedPrerenderRoutes: ReadonlySet<string>;
}; };
let prerenderGroup = 1; let prerenderGroup = 1;
@@ -1959,6 +1974,7 @@ export const onPrerenderRoute =
routesManifest, routesManifest,
isCorrectNotFoundRoutes, isCorrectNotFoundRoutes,
isEmptyAllowQueryForPrendered, isEmptyAllowQueryForPrendered,
omittedPrerenderRoutes,
} = prerenderRouteArgs; } = prerenderRouteArgs;
if (isBlocking && isFallback) { if (isBlocking && isFallback) {
@@ -2383,25 +2399,31 @@ export const onPrerenderRoute =
sourcePath = srcRoute; sourcePath = srcRoute;
} }
// The `experimentalStreamingLambdaPaths` stores the page without the let experimentalStreamingLambdaPath: string | undefined;
// leading `/` and with the `/` rewritten to be `index`. We should if (experimentalPPR) {
// normalize the key so that it matches that key in the map. if (!experimentalStreamingLambdaPaths) {
let key = srcRoute || routeKey; throw new Error(
if (key === '/') { "Invariant: experimentalStreamingLambdaPaths doesn't exist"
key = 'index'; );
} else {
if (!key.startsWith('/')) {
throw new Error("Invariant: key doesn't start with /");
} }
key = key.substring(1); // 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)) {
experimentalStreamingLambdaPath =
experimentalStreamingLambdaPaths.get(
pathnameToOutputName(entryDirectory, srcRoute)
);
} else {
experimentalStreamingLambdaPath =
experimentalStreamingLambdaPaths.get(
pathnameToOutputName(entryDirectory, routeKey)
);
}
} }
key = path.posix.join(entryDirectory, key);
const experimentalStreamingLambdaPath =
experimentalStreamingLambdaPaths?.get(key);
prerenders[outputPathPage] = new Prerender({ prerenders[outputPathPage] = new Prerender({
expiration: initialRevalidate, expiration: initialRevalidate,
lambda, lambda,
@@ -2604,6 +2626,10 @@ export async function getStaticFiles(
}; };
} }
/**
* Strips the trailing `/index` from the output name if it's not the root if
* the server mode is enabled.
*/
export function normalizeIndexOutput( export function normalizeIndexOutput(
outputName: string, outputName: string,
isServerMode: boolean isServerMode: boolean
@@ -2624,6 +2650,19 @@ export function getNextServerPath(nextVersion: string) {
: 'next/dist/next-server/server'; : 'next/dist/next-server/server';
} }
export function pathnameToOutputName(entryDirectory: string, pathname: string) {
if (pathname === '/') pathname = '/index';
return path.posix.join(entryDirectory, pathname);
}
export function getPostponeResumePathname(
entryDirectory: string,
pathname: string
): string {
if (pathname === '/') pathname = '/index';
return path.posix.join(entryDirectory, '_next/postponed/resume', pathname);
}
// update to leverage // update to leverage
export function updateRouteSrc( export function updateRouteSrc(
route: Route, route: Route,

View File

@@ -0,0 +1,18 @@
import React, { Suspense } from 'react'
import { Dynamic } from '../../../components/dynamic'
export const dynamicParams = false;
const slugs = ['a', 'b', 'c'];
export function generateStaticParams() {
return slugs.map((slug) => ({ slug }));
}
export default function NoFallbackPage({ params: { slug } }) {
return (
<Suspense fallback={<Dynamic pathname={`/no-fallback/${slug}`} fallback />}>
<Dynamic pathname={`/no-fallback/${slug}`} />
</Suspense>
)
}

View File

@@ -19,11 +19,23 @@ const pages = [
{ pathname: '/no-suspense/nested/a', dynamic: true }, { pathname: '/no-suspense/nested/a', dynamic: true },
{ pathname: '/no-suspense/nested/b', dynamic: true }, { pathname: '/no-suspense/nested/b', dynamic: true },
{ pathname: '/no-suspense/nested/c', dynamic: true }, { pathname: '/no-suspense/nested/c', dynamic: true },
{ pathname: '/no-fallback/a', dynamic: true },
{ pathname: '/no-fallback/b', dynamic: true },
{ pathname: '/no-fallback/c', dynamic: true },
// TODO: uncomment when we've fixed the 404 case for force-dynamic pages // TODO: uncomment when we've fixed the 404 case for force-dynamic pages
// { pathname: '/dynamic/force-dynamic', dynamic: 'force-dynamic' }, // { pathname: '/dynamic/force-dynamic', dynamic: 'force-dynamic' },
{ pathname: '/dynamic/force-static', dynamic: 'force-static' }, { pathname: '/dynamic/force-static', dynamic: 'force-static' },
]; ];
const cases = {
404: [
// For routes that do not support fallback (they had `dynamicParams` set to
// `false`), we shouldn't see any fallback behavior for routes not defined
// in `getStaticParams`.
{ pathname: '/no-fallback/non-existent' },
],
};
const ctx = {}; const ctx = {};
describe(`${__dirname.split(path.sep).pop()}`, () => { describe(`${__dirname.split(path.sep).pop()}`, () => {
@@ -49,6 +61,14 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
expect(html).toContain('</html>'); expect(html).toContain('</html>');
} }
); );
it.each(cases[404])(
'should return 404 for $pathname',
async ({ pathname }) => {
const res = await fetch(`${ctx.deploymentUrl}${pathname}`);
expect(res.status).toEqual(404);
}
);
}); });
describe('prefetch RSC payloads should return', () => { describe('prefetch RSC payloads should return', () => {
@@ -88,6 +108,16 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
} }
} }
); );
it.each(cases[404])(
'should return 404 for $pathname',
async ({ pathname }) => {
const res = await fetch(`${ctx.deploymentUrl}${pathname}`, {
headers: { RSC: 1, 'Next-Router-Prefetch': '1' },
});
expect(res.status).toEqual(404);
}
);
}); });
describe('dynamic RSC payloads should return', () => { describe('dynamic RSC payloads should return', () => {
@@ -122,5 +152,15 @@ describe(`${__dirname.split(path.sep).pop()}`, () => {
expect(text).not.toContain(expected); expect(text).not.toContain(expected);
} }
}); });
it.each(cases[404])(
'should return 404 for $pathname',
async ({ pathname }) => {
const res = await fetch(`${ctx.deploymentUrl}${pathname}`, {
headers: { RSC: 1 },
});
expect(res.status).toEqual(404);
}
);
}); });
}); });

View File

@@ -1,8 +1,8 @@
{ {
"dependencies": { "dependencies": {
"next": "canary", "next": "canary",
"react": "experimental", "react": "18.2.0",
"react-dom": "experimental" "react-dom": "18.2.0"
}, },
"ignoreNextjsUpdates": true "ignoreNextjsUpdates": true
} }

View File

@@ -119,16 +119,28 @@ async function disableSSO(deploymentId, useTeam = true) {
} }
); );
const text = await deployRes.text();
if (!deployRes.ok) { if (!deployRes.ok) {
throw new Error( throw new Error(
`Failed to get deployment info (status: ${ `Failed to get deployment info (status: ${deployRes.status}, body: ${text})`
deployRes.status
}, body: ${await deployRes.text()})`
); );
} }
const deploymentInfo = await deployRes.json(); let info;
const { projectId, url: deploymentUrl } = deploymentInfo; try {
info = JSON.parse(text);
} catch (err) {
throw new Error('Failed to parse deployment info JSON', { cause: err });
}
const { projectId, url: deploymentUrl } = info;
if (!deploymentUrl || typeof deploymentUrl !== 'string') {
throw new Error(
`Failed to get deployment URL (status: ${deployRes.status}, body: ${text})`
);
}
const settingRes = await fetchWithAuth( const settingRes = await fetchWithAuth(
`https://vercel.com/api/v5/projects/${encodeURIComponent(projectId)}`, `https://vercel.com/api/v5/projects/${encodeURIComponent(projectId)}`,
@@ -160,7 +172,7 @@ async function disableSSO(deploymentId, useTeam = true) {
`Disabled deployment protection for deploymentId: ${deploymentId} projectId: ${projectId}` `Disabled deployment protection for deploymentId: ${deploymentId} projectId: ${projectId}`
); );
} else { } else {
console.error(settingRes.status, await settingRes.text(), deploymentInfo); console.error(settingRes.status, await settingRes.text(), text);
throw new Error( throw new Error(
`Failed to disable deployment protection projectId: ${projectId} deploymentId ${deploymentId}` `Failed to disable deployment protection projectId: ${projectId} deploymentId ${deploymentId}`
); );