Added getRequestHandlerWithMetadata export (#10753)

This adds a new `getRequestHandlerWithMetadata` export if enabled and
available to the exported method.

---------

Co-authored-by: Joe Haddad <timer@vercel.com>
Co-authored-by: JJ Kasper <jj@jjsweb.site>
This commit is contained in:
Wyatt Johnson
2023-11-07 11:32:03 -07:00
committed by GitHub
parent 66cd626d1c
commit c94a082f6b
127 changed files with 1206 additions and 74 deletions

View File

@@ -15,6 +15,7 @@ import {
NodejsLambda,
EdgeFunction,
Images,
File,
} from '@vercel/build-utils';
import { NodeFileTraceReasons } from '@vercel/nft';
import type {
@@ -244,6 +245,7 @@ type RoutesManifestOld = {
header: string;
varyHeader: string;
prefetchHeader?: string;
didPostponeHeader?: string;
contentTypeHeader: string;
};
skipMiddlewareUrlNormalize?: boolean;
@@ -312,7 +314,8 @@ export async function getDynamicRoutes(
canUsePreviewMode?: boolean,
bypassToken?: string,
isServerMode?: boolean,
dynamicMiddlewareRouteMap?: Map<string, RouteWithSrc>
dynamicMiddlewareRouteMap?: Map<string, RouteWithSrc>,
experimentalPPR?: boolean
): Promise<RouteWithSrc[]> {
if (routesManifest) {
switch (routesManifest.version) {
@@ -385,6 +388,24 @@ export async function getDynamicRoutes(
},
];
}
if (experimentalPPR) {
let dest = route.dest?.replace(/($|\?)/, '.prefetch.rsc$1');
if (page === '/' || page === '/index') {
dest = dest?.replace(/([^/]+\.prefetch\.rsc(\?.*|$))/, '__$1');
}
routes.push({
...route,
src: route.src.replace(
new RegExp(escapeStringRegexp('(?:/)?$')),
'(?:\\.prefetch\\.rsc)(?:/)?$'
),
dest,
});
}
routes.push({
...route,
src: route.src.replace(
@@ -395,8 +416,8 @@ export async function getDynamicRoutes(
});
routes.push(route);
continue;
}
return routes;
}
default: {
@@ -778,7 +799,8 @@ export async function createPseudoLayer(files: {
return { pseudoLayer, pseudoLayerBytes };
}
interface CreateLambdaFromPseudoLayersOptions extends LambdaOptionsWithFiles {
export interface CreateLambdaFromPseudoLayersOptions
extends LambdaOptionsWithFiles {
layers: PseudoLayer[];
isStreaming?: boolean;
nextVersion?: string;
@@ -858,10 +880,12 @@ export type NextPrerenderedRoutes = {
[route: string]: {
initialRevalidate: number | false;
dataRoute: string | null;
prefetchDataRoute?: string | null;
srcRoute: string | null;
initialStatus?: number;
initialHeaders?: Record<string, string>;
experimentalBypassFor?: HasField;
experimentalPPR?: boolean;
};
};
@@ -870,7 +894,10 @@ export type NextPrerenderedRoutes = {
routeRegex: string;
dataRoute: string | null;
dataRouteRegex: string | null;
prefetchDataRoute?: string | null;
prefetchDataRouteRegex?: string | null;
experimentalBypassFor?: HasField;
experimentalPPR?: boolean;
};
};
@@ -880,7 +907,10 @@ export type NextPrerenderedRoutes = {
routeRegex: string;
dataRoute: string | null;
dataRouteRegex: string | null;
prefetchDataRoute?: string | null;
prefetchDataRouteRegex?: string | null;
experimentalBypassFor?: HasField;
experimentalPPR?: boolean;
};
};
@@ -889,7 +919,10 @@ export type NextPrerenderedRoutes = {
routeRegex: string;
dataRoute: string | null;
dataRouteRegex: string | null;
prefetchDataRoute: string | null | undefined;
prefetchDataRouteRegex: string | null | undefined;
experimentalBypassFor?: HasField;
experimentalPPR?: boolean;
};
};
@@ -1091,9 +1124,11 @@ export async function getPrerenderManifest(
initialRevalidateSeconds: number | false;
srcRoute: string | null;
dataRoute: string | null;
prefetchDataRoute: string | null | undefined;
initialStatus?: number;
initialHeaders?: Record<string, string>;
experimentalBypassFor?: HasField;
experimentalPPR?: boolean;
};
};
dynamicRoutes: {
@@ -1102,7 +1137,10 @@ export async function getPrerenderManifest(
fallback: string | false;
dataRoute: string | null;
dataRouteRegex: string | null;
prefetchDataRoute: string | null | undefined;
prefetchDataRouteRegex: string | null | undefined;
experimentalBypassFor?: HasField;
experimentalPPR?: boolean;
};
};
preview: {
@@ -1189,11 +1227,15 @@ export async function getPrerenderManifest(
let initialStatus: undefined | number;
let initialHeaders: undefined | Record<string, string>;
let experimentalBypassFor: undefined | HasField;
let experimentalPPR: undefined | boolean;
let prefetchDataRoute: undefined | string | null;
if (manifest.version === 4) {
initialStatus = manifest.routes[route].initialStatus;
initialHeaders = manifest.routes[route].initialHeaders;
experimentalBypassFor = manifest.routes[route].experimentalBypassFor;
experimentalPPR = manifest.routes[route].experimentalPPR;
prefetchDataRoute = manifest.routes[route].prefetchDataRoute;
}
ret.staticRoutes[route] = {
@@ -1202,10 +1244,12 @@ export async function getPrerenderManifest(
? false
: Math.max(1, initialRevalidateSeconds),
dataRoute,
prefetchDataRoute,
srcRoute,
initialStatus,
initialHeaders,
experimentalBypassFor,
experimentalPPR,
};
});
@@ -1213,35 +1257,52 @@ export async function getPrerenderManifest(
const { routeRegex, fallback, dataRoute, dataRouteRegex } =
manifest.dynamicRoutes[lazyRoute];
let experimentalBypassFor: undefined | HasField;
let experimentalPPR: undefined | boolean;
let prefetchDataRoute: undefined | string | null;
let prefetchDataRouteRegex: undefined | string | null;
if (manifest.version === 4) {
experimentalBypassFor =
manifest.dynamicRoutes[lazyRoute].experimentalBypassFor;
experimentalPPR = manifest.dynamicRoutes[lazyRoute].experimentalPPR;
prefetchDataRoute =
manifest.dynamicRoutes[lazyRoute].prefetchDataRoute;
prefetchDataRouteRegex =
manifest.dynamicRoutes[lazyRoute].prefetchDataRouteRegex;
}
if (typeof fallback === 'string') {
ret.fallbackRoutes[lazyRoute] = {
experimentalBypassFor,
experimentalPPR,
routeRegex,
fallback,
dataRoute,
dataRouteRegex,
prefetchDataRoute,
prefetchDataRouteRegex,
};
} else if (fallback === null) {
ret.blockingFallbackRoutes[lazyRoute] = {
experimentalBypassFor,
experimentalPPR,
routeRegex,
dataRoute,
dataRouteRegex,
prefetchDataRoute,
prefetchDataRouteRegex,
};
} else {
// Fallback behavior is disabled, all routes would've been provided
// in the top-level `routes` key (`staticRoutes`).
ret.omittedRoutes[lazyRoute] = {
experimentalBypassFor,
experimentalPPR,
routeRegex,
dataRoute,
dataRouteRegex,
prefetchDataRoute,
prefetchDataRouteRegex,
};
}
});
@@ -1417,6 +1478,7 @@ export type LambdaGroup = {
isAppRouteHandler?: boolean;
isStreaming?: boolean;
isPrerenders?: boolean;
isExperimentalPPR?: boolean;
isPages?: boolean;
isApiLambda: boolean;
pseudoLayer: PseudoLayer;
@@ -1430,6 +1492,7 @@ export async function getPageLambdaGroups({
functionsConfigManifest,
pages,
prerenderRoutes,
experimentalPPRRoutes,
pageTraces,
compressedPages,
tracedPseudoLayer,
@@ -1444,6 +1507,7 @@ export async function getPageLambdaGroups({
functionsConfigManifest?: FunctionsConfigManifestV1;
pages: string[];
prerenderRoutes: Set<string>;
experimentalPPRRoutes: Set<string> | undefined;
pageTraces: {
[page: string]: {
[key: string]: FileFsRef;
@@ -1465,6 +1529,7 @@ export async function getPageLambdaGroups({
const newPages = [...internalPages, page];
const routeName = normalizePage(page.replace(/\.js$/, ''));
const isPrerenderRoute = prerenderRoutes.has(routeName);
const isExperimentalPPR = experimentalPPRRoutes?.has(routeName) ?? false;
let opts: { memory?: number; maxDuration?: number } = {};
@@ -1494,7 +1559,8 @@ export async function getPageLambdaGroups({
const matches =
group.maxDuration === opts.maxDuration &&
group.memory === opts.memory &&
group.isPrerenders === isPrerenderRoute;
group.isPrerenders === isPrerenderRoute &&
group.isExperimentalPPR === isExperimentalPPR;
if (matches) {
let newTracedFilesSize = group.pseudoLayerBytes;
@@ -1533,6 +1599,7 @@ export async function getPageLambdaGroups({
pages: [page],
...opts,
isPrerenders: isPrerenderRoute,
isExperimentalPPR,
isApiLambda: !!isApiPage(page),
pseudoLayerBytes: initialPseudoLayer.pseudoLayerBytes,
pseudoLayerUncompressedBytes: initialPseudoLayerUncompressed,
@@ -1831,7 +1898,8 @@ type OnPrerenderRouteArgs = {
isServerMode: boolean;
canUsePreviewMode: boolean;
lambdas: { [key: string]: Lambda };
prerenders: { [key: string]: Prerender | FileFsRef };
experimentalStreamingLambdaPaths: Map<string, string> | undefined;
prerenders: { [key: string]: Prerender | File };
pageLambdaMap: { [key: string]: string };
routesManifest?: RoutesManifest;
isCorrectNotFoundRoutes?: boolean;
@@ -1841,7 +1909,7 @@ let prerenderGroup = 1;
export const onPrerenderRoute =
(prerenderRouteArgs: OnPrerenderRouteArgs) =>
(
async (
routeKey: string,
{
isBlocking,
@@ -1866,6 +1934,7 @@ export const onPrerenderRoute =
isServerMode,
canUsePreviewMode,
lambdas,
experimentalStreamingLambdaPaths,
prerenders,
pageLambdaMap,
routesManifest,
@@ -1926,9 +1995,11 @@ export const onPrerenderRoute =
let initialRevalidate: false | number;
let srcRoute: string | null;
let dataRoute: string | null;
let prefetchDataRoute: string | null | undefined;
let initialStatus: number | undefined;
let initialHeaders: Record<string, string> | undefined;
let experimentalBypassFor: HasField | undefined;
let experimentalPPR: boolean | undefined;
if (isFallback || isBlocking) {
const pr = isFallback
@@ -1946,12 +2017,18 @@ export const onPrerenderRoute =
srcRoute = null;
dataRoute = pr.dataRoute;
experimentalBypassFor = pr.experimentalBypassFor;
experimentalPPR = pr.experimentalPPR;
prefetchDataRoute = pr.prefetchDataRoute;
} else if (isOmitted) {
initialRevalidate = false;
srcRoute = routeKey;
dataRoute = prerenderManifest.omittedRoutes[routeKey].dataRoute;
experimentalBypassFor =
prerenderManifest.omittedRoutes[routeKey].experimentalBypassFor;
experimentalPPR =
prerenderManifest.omittedRoutes[routeKey].experimentalPPR;
prefetchDataRoute =
prerenderManifest.omittedRoutes[routeKey].prefetchDataRoute;
} else {
const pr = prerenderManifest.staticRoutes[routeKey];
({
@@ -1961,19 +2038,71 @@ export const onPrerenderRoute =
initialHeaders,
initialStatus,
experimentalBypassFor,
experimentalPPR,
prefetchDataRoute,
} = pr);
}
let isAppPathRoute = false;
// TODO: leverage manifest to determine app paths more accurately
if (appDir && srcRoute && (!dataRoute || dataRoute?.endsWith('.rsc'))) {
isAppPathRoute = true;
}
const isOmittedOrNotFound = isOmitted || isNotFound;
let htmlFsRef: FileFsRef | null;
let htmlFsRef: File | null;
if (appDir && !dataRoute && isAppPathRoute && !(isBlocking || isFallback)) {
// If enabled, try to get the postponed route information from the file
// system and use it to assemble the prerender.
let prerender: string | undefined;
if (experimentalPPR && appDir) {
const htmlPath = path.join(appDir, `${routeFileNoExt}.html`);
const metaPath = path.join(appDir, `${routeFileNoExt}.meta`);
if (fs.existsSync(htmlPath) && fs.existsSync(metaPath)) {
const meta = JSON.parse(await fs.readFile(metaPath, 'utf8'));
if ('postponed' in meta && typeof meta.postponed === 'string') {
prerender = meta.postponed;
// Assign the headers Content-Type header to the prerendered type.
initialHeaders ??= {};
initialHeaders[
'content-type'
] = `application/x-nextjs-pre-render; state-length=${meta.postponed.length}`;
// Read the HTML file and append it to the prerendered content.
const html = await fs.readFileSync(htmlPath, 'utf8');
prerender += html;
}
}
if (!dataRoute?.endsWith('.rsc')) {
throw new Error(
`Invariant: unexpected output path for ${dataRoute} and PPR`
);
}
if (!prefetchDataRoute?.endsWith('.prefetch.rsc')) {
throw new Error(
`Invariant: unexpected output path for ${prefetchDataRoute} and PPR`
);
}
}
if (prerender) {
const contentType = initialHeaders?.['content-type'];
if (!contentType) {
throw new Error("Invariant: contentType can't be undefined");
}
// Assemble the prerendered file.
htmlFsRef = new FileBlob({ contentType, data: prerender });
} else if (
appDir &&
!dataRoute &&
isAppPathRoute &&
!(isBlocking || isFallback)
) {
const contentType = initialHeaders?.['content-type'];
htmlFsRef = new FileFsRef({
fsPath: path.join(appDir, `${routeFileNoExt}.body`),
@@ -2023,7 +2152,7 @@ export const onPrerenderRoute =
? addLocaleOrDefault('/404.html', routesManifest, locale)
: '/404.html'
: isAppPathRoute
? dataRoute
? prefetchDataRoute || dataRoute
: routeFileNoExt + '.json'
}`
),
@@ -2054,13 +2183,12 @@ export const onPrerenderRoute =
);
let lambda: undefined | Lambda;
let outputPathData: null | string = null;
if (dataRoute) {
outputPathData = path.posix.join(entryDirectory, dataRoute);
function normalizeDataRoute(route: string) {
let normalized = path.posix.join(entryDirectory, route);
if (nonDynamicSsg || isFallback || isOmitted) {
outputPathData = outputPathData.replace(
normalized = normalized.replace(
new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`),
// ensure we escape "$" correctly while replacing as "$" is a special
// character, we need to do double escaping as first is for the initial
@@ -2068,8 +2196,32 @@ export const onPrerenderRoute =
`${routeFileNoExt.replace(/\$/g, '$$$$')}.json`
);
}
return normalized;
}
let outputPathData: null | string = null;
if (dataRoute) {
outputPathData = normalizeDataRoute(dataRoute);
}
let outputPathPrefetchData: null | string = null;
if (prefetchDataRoute) {
if (!experimentalPPR) {
throw new Error(
"Invariant: prefetchDataRoute can't be set without PPR"
);
}
outputPathPrefetchData = normalizeDataRoute(prefetchDataRoute);
} else if (experimentalPPR) {
throw new Error('Invariant: expected to find prefetch data route PPR');
}
// When the prefetch data path is available, use it for the prerender,
// otherwise use the data path.
const outputPrerenderPathData = outputPathPrefetchData || outputPathData;
if (isSharedLambdas) {
const outputSrcPathPage = normalizeIndexOutput(
path.join(
@@ -2117,8 +2269,8 @@ export const onPrerenderRoute =
htmlFsRef.contentType = htmlContentType;
prerenders[outputPathPage] = htmlFsRef;
if (outputPathData) {
prerenders[outputPathData] = jsonFsRef;
if (outputPrerenderPathData) {
prerenders[outputPrerenderPathData] = jsonFsRef;
}
}
}
@@ -2188,12 +2340,32 @@ export const onPrerenderRoute =
'RSC, Next-Router-State-Tree, Next-Router-Prefetch';
const rscContentTypeHeader =
routesManifest?.rsc?.contentTypeHeader || RSC_CONTENT_TYPE;
const rscDidPostponeHeader = routesManifest?.rsc?.didPostponeHeader;
let sourcePath: string | undefined;
if (`/${outputPathPage}` !== srcRoute && srcRoute) {
sourcePath = srcRoute;
}
// The `experimentalStreamingLambdaPaths` stores the page without the
// leading `/` and with the `/` rewritten to be `index`. We should
// normalize the key so that it matches that key in the map.
let key = srcRoute || routeKey;
if (key === '/') {
key = 'index';
} else {
if (!key.startsWith('/')) {
throw new Error("Invariant: key doesn't start with /");
}
key = key.substring(1);
}
key = path.posix.join(entryDirectory, key);
const experimentalStreamingLambdaPath =
experimentalStreamingLambdaPaths?.get(key);
prerenders[outputPathPage] = new Prerender({
expiration: initialRevalidate,
lambda,
@@ -2205,6 +2377,7 @@ export const onPrerenderRoute =
initialStatus,
initialHeaders,
sourcePath,
experimentalStreamingLambdaPath,
...(isNotFound
? {
@@ -2222,8 +2395,21 @@ export const onPrerenderRoute =
: {}),
});
if (outputPathData) {
prerenders[outputPathData] = new Prerender({
if (outputPrerenderPathData) {
let normalizedPathData = outputPrerenderPathData;
if (
(srcRoute === '/' || srcRoute == '/index') &&
outputPrerenderPathData.endsWith(RSC_PREFETCH_SUFFIX)
) {
delete lambdas[normalizedPathData];
normalizedPathData = normalizedPathData.replace(
/([^/]+\.prefetch\.rsc)$/,
'__$1'
);
}
prerenders[normalizedPathData] = new Prerender({
expiration: initialRevalidate,
lambda,
allowQuery,
@@ -2243,6 +2429,9 @@ export const onPrerenderRoute =
initialHeaders: {
'content-type': rscContentTypeHeader,
vary: rscVaryHeader,
...(experimentalPPR && rscDidPostponeHeader
? { [rscDidPostponeHeader]: '1' }
: {}),
},
}
: {}),