mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-06 12:57:46 +00:00
[next] Support pre-generated pages without fallbacks with Partial Prerendering (#11183)
This commit is contained in:
5
.changeset/calm-terms-dance.md
Normal file
5
.changeset/calm-terms-dance.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@vercel/next': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Enable partial prerendering support for pre-generated pages
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
// We want to add the `experimentalStreamingLambdaPath` to this
|
||||||
// output.
|
// output.
|
||||||
experimentalStreamingLambdaPaths.set(outputName, pprOutputName);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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),
|
||||||
|
|||||||
@@ -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';
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
} else {
|
||||||
if (!key.startsWith('/')) {
|
experimentalStreamingLambdaPath =
|
||||||
throw new Error("Invariant: key doesn't start with /");
|
experimentalStreamingLambdaPaths.get(
|
||||||
|
pathnameToOutputName(entryDirectory, routeKey)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
key = key.substring(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
18
packages/next/test/fixtures/00-app-dir-ppr-full/app/no-fallback/[slug]/page.jsx
vendored
Normal file
18
packages/next/test/fixtures/00-app-dir-ppr-full/app/no-fallback/[slug]/page.jsx
vendored
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user