mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-07 21:07:46 +00:00
This fixes a regression introduced by #11625 that enabled streaming for routes on the pages router (these routes do not support streaming).
3495 lines
97 KiB
TypeScript
3495 lines
97 KiB
TypeScript
import {
|
|
FileFsRef,
|
|
Files,
|
|
Config,
|
|
debug,
|
|
FileBlob,
|
|
glob,
|
|
Lambda,
|
|
Prerender,
|
|
getLambdaOptionsFromFunction,
|
|
getPlatformEnv,
|
|
streamToBuffer,
|
|
NowBuildError,
|
|
isSymbolicLink,
|
|
NodejsLambda,
|
|
EdgeFunction,
|
|
Images,
|
|
File,
|
|
FlagDefinitions,
|
|
} from '@vercel/build-utils';
|
|
import { NodeFileTraceReasons } from '@vercel/nft';
|
|
import type {
|
|
HasField,
|
|
Header,
|
|
Rewrite,
|
|
Route,
|
|
RouteWithSrc,
|
|
} from '@vercel/routing-utils';
|
|
import { Sema } from 'async-sema';
|
|
import crc32 from 'buffer-crc32';
|
|
import fs, { lstat, stat } from 'fs-extra';
|
|
import path from 'path';
|
|
import semver from 'semver';
|
|
import url from 'url';
|
|
import { createRequire } from 'module';
|
|
import escapeStringRegexp from 'escape-string-regexp';
|
|
import { htmlContentType } from '.';
|
|
import textTable from 'text-table';
|
|
import { getNextjsEdgeFunctionSource } from './edge-function-source/get-edge-function-source';
|
|
import type { LambdaOptionsWithFiles } from '@vercel/build-utils/dist/lambda';
|
|
import { stringifySourceMap } from './sourcemapped';
|
|
import type { RawSourceMap } from 'source-map';
|
|
import { prettyBytes } from './pretty-bytes';
|
|
import {
|
|
MIB,
|
|
KIB,
|
|
LAMBDA_RESERVED_UNCOMPRESSED_SIZE,
|
|
DEFAULT_MAX_UNCOMPRESSED_LAMBDA_SIZE,
|
|
} from './constants';
|
|
|
|
type stringMap = { [key: string]: string };
|
|
|
|
export const require_ = createRequire(__filename);
|
|
|
|
export const RSC_CONTENT_TYPE = 'x-component';
|
|
export const RSC_PREFETCH_SUFFIX = '.prefetch.rsc';
|
|
|
|
export const MAX_UNCOMPRESSED_LAMBDA_SIZE = !isNaN(
|
|
Number(process.env.MAX_UNCOMPRESSED_LAMBDA_SIZE)
|
|
)
|
|
? Number(process.env.MAX_UNCOMPRESSED_LAMBDA_SIZE)
|
|
: DEFAULT_MAX_UNCOMPRESSED_LAMBDA_SIZE;
|
|
|
|
// Identify /[param]/ in route string
|
|
// eslint-disable-next-line no-useless-escape
|
|
const TEST_DYNAMIC_ROUTE = /\/\[[^\/]+?\](?=\/|$)/;
|
|
|
|
function isDynamicRoute(route: string): boolean {
|
|
route = route.startsWith('/') ? route : `/${route}`;
|
|
return TEST_DYNAMIC_ROUTE.test(route);
|
|
}
|
|
|
|
/**
|
|
* Validate if the entrypoint is allowed to be used
|
|
*/
|
|
function validateEntrypoint(entrypoint: string) {
|
|
if (
|
|
!/package\.json$/.exec(entrypoint) &&
|
|
!/next\.config\.js$/.exec(entrypoint)
|
|
) {
|
|
throw new NowBuildError({
|
|
message:
|
|
'Specified "src" for "@vercel/next" has to be "package.json" or "next.config.js"',
|
|
code: 'NEXT_INCORRECT_SRC',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exclude certain files from the files object
|
|
*/
|
|
function excludeFiles(
|
|
files: Files,
|
|
matcher: (filePath: string) => boolean
|
|
): Files {
|
|
return Object.keys(files).reduce((newFiles, filePath) => {
|
|
if (matcher(filePath)) {
|
|
return newFiles;
|
|
}
|
|
return {
|
|
...newFiles,
|
|
[filePath]: files[filePath],
|
|
};
|
|
}, {});
|
|
}
|
|
|
|
/**
|
|
* Enforce specific package.json configuration for smallest possible lambda
|
|
*/
|
|
function normalizePackageJson(
|
|
defaultPackageJson: {
|
|
dependencies?: stringMap;
|
|
devDependencies?: stringMap;
|
|
scripts?: stringMap;
|
|
} = {}
|
|
) {
|
|
const dependencies: stringMap = {};
|
|
const devDependencies: stringMap = {
|
|
...defaultPackageJson.dependencies,
|
|
...defaultPackageJson.devDependencies,
|
|
};
|
|
|
|
if (devDependencies.react) {
|
|
dependencies.react = devDependencies.react;
|
|
delete devDependencies.react;
|
|
}
|
|
|
|
if (devDependencies['react-dom']) {
|
|
dependencies['react-dom'] = devDependencies['react-dom'];
|
|
delete devDependencies['react-dom'];
|
|
}
|
|
|
|
delete devDependencies['next-server'];
|
|
|
|
return {
|
|
...defaultPackageJson,
|
|
dependencies: {
|
|
// react and react-dom can be overwritten
|
|
react: 'latest',
|
|
'react-dom': 'latest',
|
|
...dependencies, // override react if user provided it
|
|
// next-server is forced to canary
|
|
'next-server': 'v7.0.2-canary.49',
|
|
},
|
|
devDependencies: {
|
|
...devDependencies,
|
|
// next is forced to canary
|
|
next: 'v7.0.2-canary.49',
|
|
},
|
|
scripts: {
|
|
...defaultPackageJson.scripts,
|
|
'now-build':
|
|
'NODE_OPTIONS=--max_old_space_size=3000 next build --lambdas',
|
|
},
|
|
};
|
|
}
|
|
|
|
async function getNextConfig(workPath: string, entryPath: string) {
|
|
const entryConfig = path.join(entryPath, './next.config.js');
|
|
if (await fs.pathExists(entryConfig)) {
|
|
return fs.readFile(entryConfig, 'utf8');
|
|
}
|
|
|
|
const workConfig = path.join(workPath, './next.config.js');
|
|
if (await fs.pathExists(workConfig)) {
|
|
return fs.readFile(workConfig, 'utf8');
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function getImagesConfig(
|
|
imagesManifest: NextImagesManifest | undefined
|
|
): Images | undefined {
|
|
return imagesManifest?.images?.loader === 'default' &&
|
|
imagesManifest.images?.unoptimized !== true
|
|
? {
|
|
domains: imagesManifest.images.domains,
|
|
sizes: imagesManifest.images.sizes,
|
|
remotePatterns: imagesManifest.images.remotePatterns,
|
|
minimumCacheTTL: imagesManifest.images.minimumCacheTTL,
|
|
formats: imagesManifest.images.formats,
|
|
dangerouslyAllowSVG: imagesManifest.images.dangerouslyAllowSVG,
|
|
contentSecurityPolicy: imagesManifest.images.contentSecurityPolicy,
|
|
contentDispositionType: imagesManifest.images.contentDispositionType,
|
|
}
|
|
: undefined;
|
|
}
|
|
|
|
function normalizePage(page: string): string {
|
|
// Resolve on anything that doesn't start with `/`
|
|
if (!page.startsWith('/')) {
|
|
page = `/${page}`;
|
|
}
|
|
|
|
// Replace the `/index` with `/`
|
|
if (page === '/index') {
|
|
page = '/';
|
|
}
|
|
|
|
return page;
|
|
}
|
|
|
|
export type Redirect = Rewrite & {
|
|
statusCode?: number;
|
|
permanent?: boolean;
|
|
};
|
|
|
|
type RoutesManifestRegex = {
|
|
regex: string;
|
|
regexKeys: string[];
|
|
};
|
|
|
|
type RoutesManifestRoute = {
|
|
page: string;
|
|
regex: string;
|
|
namedRegex?: string;
|
|
routeKeys?: { [named: string]: string };
|
|
};
|
|
|
|
type RoutesManifestOld = {
|
|
pages404: boolean;
|
|
basePath: string | undefined;
|
|
redirects: (Redirect & RoutesManifestRegex)[];
|
|
rewrites:
|
|
| (Rewrite & RoutesManifestRegex)[]
|
|
| {
|
|
beforeFiles: (Rewrite & RoutesManifestRegex)[];
|
|
afterFiles: (Rewrite & RoutesManifestRegex)[];
|
|
fallback: (Rewrite & RoutesManifestRegex)[];
|
|
};
|
|
headers?: (Header & RoutesManifestRegex)[];
|
|
dynamicRoutes: RoutesManifestRoute[];
|
|
staticRoutes: RoutesManifestRoute[];
|
|
version: 1 | 2 | 3;
|
|
dataRoutes?: Array<{
|
|
page: string;
|
|
dataRouteRegex: string;
|
|
namedDataRouteRegex?: string;
|
|
routeKeys?: { [named: string]: string };
|
|
}>;
|
|
i18n?: {
|
|
localeDetection?: boolean;
|
|
defaultLocale: string;
|
|
locales: string[];
|
|
domains?: Array<{
|
|
http?: boolean;
|
|
domain: string;
|
|
locales?: string[];
|
|
defaultLocale: string;
|
|
}>;
|
|
};
|
|
rsc?: {
|
|
header: string;
|
|
varyHeader: string;
|
|
prefetchHeader?: string;
|
|
didPostponeHeader?: string;
|
|
contentTypeHeader: string;
|
|
};
|
|
skipMiddlewareUrlNormalize?: boolean;
|
|
};
|
|
|
|
type RoutesManifestV4 = Omit<RoutesManifestOld, 'dynamicRoutes' | 'version'> & {
|
|
version: 4;
|
|
dynamicRoutes: (RoutesManifestRoute | { page: string; isMiddleware: true })[];
|
|
};
|
|
|
|
export type RoutesManifest = RoutesManifestV4 | RoutesManifestOld;
|
|
|
|
export async function getRoutesManifest(
|
|
entryPath: string,
|
|
outputDirectory: string,
|
|
nextVersion?: string
|
|
): Promise<RoutesManifest | undefined> {
|
|
const shouldHaveManifest =
|
|
nextVersion && semver.gte(nextVersion, '9.1.4-canary.0');
|
|
if (!shouldHaveManifest) return;
|
|
|
|
const pathRoutesManifest = path.join(
|
|
entryPath,
|
|
outputDirectory,
|
|
'routes-manifest.json'
|
|
);
|
|
const hasRoutesManifest = await fs
|
|
.access(pathRoutesManifest)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (shouldHaveManifest && !hasRoutesManifest) {
|
|
throw new NowBuildError({
|
|
message: `The file "${pathRoutesManifest}" couldn't be found. This is often caused by a misconfiguration in your project.`,
|
|
link: 'https://err.sh/vercel/vercel/now-next-routes-manifest',
|
|
code: 'NEXT_NO_ROUTES_MANIFEST',
|
|
});
|
|
}
|
|
|
|
const routesManifest: RoutesManifest = await fs.readJSON(pathRoutesManifest);
|
|
// remove temporary array based routeKeys from v1/v2 of routes
|
|
// manifest since it can result in invalid routes
|
|
for (const route of routesManifest.dataRoutes || []) {
|
|
if (Array.isArray(route.routeKeys)) {
|
|
delete route.routeKeys;
|
|
delete route.namedDataRouteRegex;
|
|
}
|
|
}
|
|
for (const route of routesManifest.dynamicRoutes || []) {
|
|
if ('routeKeys' in route && Array.isArray(route.routeKeys)) {
|
|
delete route.routeKeys;
|
|
delete route.namedRegex;
|
|
}
|
|
}
|
|
|
|
return routesManifest;
|
|
}
|
|
|
|
export async function getDynamicRoutes({
|
|
entryPath,
|
|
entryDirectory,
|
|
dynamicPages,
|
|
isDev,
|
|
routesManifest,
|
|
omittedRoutes,
|
|
canUsePreviewMode,
|
|
bypassToken,
|
|
isServerMode,
|
|
dynamicMiddlewareRouteMap,
|
|
hasActionOutputSupport,
|
|
isAppPPREnabled,
|
|
}: {
|
|
entryPath: string;
|
|
entryDirectory: string;
|
|
dynamicPages: string[];
|
|
isDev?: boolean;
|
|
routesManifest?: RoutesManifest;
|
|
omittedRoutes?: ReadonlySet<string>;
|
|
canUsePreviewMode?: boolean;
|
|
bypassToken?: string;
|
|
isServerMode?: boolean;
|
|
dynamicMiddlewareRouteMap?: ReadonlyMap<string, RouteWithSrc>;
|
|
hasActionOutputSupport: boolean;
|
|
isAppPPREnabled: boolean;
|
|
}): Promise<RouteWithSrc[]> {
|
|
if (routesManifest) {
|
|
switch (routesManifest.version) {
|
|
case 1:
|
|
case 2: {
|
|
return routesManifest.dynamicRoutes
|
|
.filter(({ page }) => canUsePreviewMode || !omittedRoutes?.has(page))
|
|
.map(({ page, regex }: { page: string; regex: string }) => {
|
|
return {
|
|
src: regex,
|
|
dest: !isDev ? path.posix.join('/', entryDirectory, page) : page,
|
|
check: true,
|
|
status:
|
|
canUsePreviewMode && omittedRoutes?.has(page) ? 404 : undefined,
|
|
};
|
|
});
|
|
}
|
|
case 3:
|
|
case 4: {
|
|
const routes: RouteWithSrc[] = [];
|
|
|
|
for (const dynamicRoute of routesManifest.dynamicRoutes) {
|
|
if (!canUsePreviewMode && omittedRoutes?.has(dynamicRoute.page)) {
|
|
continue;
|
|
}
|
|
const params = dynamicRoute;
|
|
|
|
if ('isMiddleware' in params) {
|
|
const route = dynamicMiddlewareRouteMap?.get(params.page);
|
|
if (!route) {
|
|
throw new Error(
|
|
`Could not find dynamic middleware route for ${params.page}`
|
|
);
|
|
}
|
|
|
|
routes.push(route);
|
|
continue;
|
|
}
|
|
|
|
const { page, namedRegex, regex, routeKeys } = params;
|
|
const route: RouteWithSrc = {
|
|
src: namedRegex || regex,
|
|
dest: `${
|
|
!isDev ? path.posix.join('/', entryDirectory, page) : page
|
|
}${
|
|
routeKeys
|
|
? `?${Object.keys(routeKeys)
|
|
.map(key => `${routeKeys[key]}=$${key}`)
|
|
.join('&')}`
|
|
: ''
|
|
}`,
|
|
};
|
|
|
|
if (!isServerMode) {
|
|
route.check = true;
|
|
}
|
|
|
|
if (isServerMode && canUsePreviewMode && omittedRoutes?.has(page)) {
|
|
// only match this route when in preview mode so
|
|
// preview works for non-prerender fallback: false pages
|
|
route.has = [
|
|
{
|
|
type: 'cookie',
|
|
key: '__prerender_bypass',
|
|
value: bypassToken || undefined,
|
|
},
|
|
{
|
|
type: 'cookie',
|
|
key: '__next_preview_data',
|
|
},
|
|
];
|
|
}
|
|
|
|
if (isAppPPREnabled) {
|
|
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,
|
|
});
|
|
}
|
|
|
|
if (hasActionOutputSupport) {
|
|
routes.push({
|
|
...route,
|
|
src: route.src.replace(
|
|
new RegExp(escapeStringRegexp('(?:/)?$')),
|
|
'(?<nxtsuffix>(?:\\.action|\\.rsc))(?:/)?$'
|
|
),
|
|
dest: route.dest?.replace(/($|\?)/, '$nxtsuffix$1'),
|
|
});
|
|
} else {
|
|
routes.push({
|
|
...route,
|
|
src: route.src.replace(
|
|
new RegExp(escapeStringRegexp('(?:/)?$')),
|
|
'(?:\\.rsc)(?:/)?$'
|
|
),
|
|
dest: route.dest?.replace(/($|\?)/, '.rsc$1'),
|
|
});
|
|
}
|
|
|
|
routes.push(route);
|
|
}
|
|
|
|
return routes;
|
|
}
|
|
default: {
|
|
// update MIN_ROUTES_MANIFEST_VERSION
|
|
throw new NowBuildError({
|
|
message:
|
|
'This version of `@vercel/next` does not support the version of Next.js you are trying to deploy.\n' +
|
|
'Please upgrade your `@vercel/next` builder and try again. Contact support if this continues to happen.',
|
|
code: 'NEXT_VERSION_UPGRADE',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// FALLBACK:
|
|
// When `routes-manifest.json` does not exist (old Next.js versions), we'll try to
|
|
// require the methods we need from Next.js' internals.
|
|
if (!dynamicPages.length) {
|
|
return [];
|
|
}
|
|
|
|
let getRouteRegex: ((pageName: string) => { re: RegExp }) | undefined =
|
|
undefined;
|
|
|
|
let getSortedRoutes:
|
|
| ((normalizedPages: ReadonlyArray<string>) => string[])
|
|
| undefined;
|
|
|
|
try {
|
|
const resolved = require_.resolve('next-server/dist/lib/router/utils', {
|
|
paths: [entryPath],
|
|
});
|
|
({ getRouteRegex, getSortedRoutes } = require_(resolved));
|
|
if (typeof getRouteRegex !== 'function') {
|
|
getRouteRegex = undefined;
|
|
}
|
|
} catch (_) {} // eslint-disable-line no-empty
|
|
|
|
if (!getRouteRegex || !getSortedRoutes) {
|
|
try {
|
|
const resolved = require_.resolve(
|
|
'next/dist/next-server/lib/router/utils',
|
|
{ paths: [entryPath] }
|
|
);
|
|
({ getRouteRegex, getSortedRoutes } = require_(resolved));
|
|
if (typeof getRouteRegex !== 'function') {
|
|
getRouteRegex = undefined;
|
|
}
|
|
} catch (_) {} // eslint-disable-line no-empty
|
|
}
|
|
|
|
if (!getRouteRegex || !getSortedRoutes) {
|
|
throw new NowBuildError({
|
|
message:
|
|
'Found usage of dynamic routes but not on a new enough version of Next.js.',
|
|
code: 'NEXT_DYNAMIC_ROUTES_OUTDATED',
|
|
});
|
|
}
|
|
|
|
const pageMatchers = getSortedRoutes(dynamicPages).map(pageName => ({
|
|
pageName,
|
|
matcher: getRouteRegex && getRouteRegex(pageName).re,
|
|
}));
|
|
|
|
const routes: RouteWithSrc[] = [];
|
|
pageMatchers.forEach(pageMatcher => {
|
|
// in `vercel dev` we don't need to prefix the destination
|
|
const dest = !isDev
|
|
? path.posix.join('/', entryDirectory, pageMatcher.pageName)
|
|
: pageMatcher.pageName;
|
|
|
|
if (pageMatcher && pageMatcher.matcher) {
|
|
routes.push({
|
|
src: pageMatcher.matcher.source,
|
|
dest,
|
|
check: !isDev,
|
|
});
|
|
}
|
|
});
|
|
return routes;
|
|
}
|
|
|
|
export function localizeDynamicRoutes(
|
|
dynamicRoutes: RouteWithSrc[],
|
|
dynamicPrefix: string,
|
|
entryDirectory: string,
|
|
staticPages: Files,
|
|
prerenderManifest: NextPrerenderedRoutes,
|
|
routesManifest?: RoutesManifest,
|
|
isServerMode?: boolean,
|
|
isCorrectLocaleAPIRoutes?: boolean,
|
|
inversedAppPathRoutesManifest?: Record<string, string>
|
|
): RouteWithSrc[] {
|
|
return dynamicRoutes.map((route: RouteWithSrc) => {
|
|
// i18n is already handled for middleware
|
|
if (route.middleware !== undefined || route.middlewarePath !== undefined)
|
|
return route;
|
|
|
|
const { i18n } = routesManifest || {};
|
|
|
|
if (i18n) {
|
|
const { pathname } = url.parse(route.dest!);
|
|
const pathnameNoPrefix = pathname?.replace(dynamicPrefix, '');
|
|
const isFallback = prerenderManifest.fallbackRoutes[pathname!];
|
|
const isBlocking = prerenderManifest.blockingFallbackRoutes[pathname!];
|
|
const isApiRoute =
|
|
pathnameNoPrefix === '/api' || pathnameNoPrefix?.startsWith('/api/');
|
|
const isAutoExport =
|
|
staticPages[addLocaleOrDefault(pathname!, routesManifest).substring(1)];
|
|
|
|
const isAppRoute =
|
|
inversedAppPathRoutesManifest?.[pathnameNoPrefix || ''];
|
|
|
|
const isLocalePrefixed =
|
|
isFallback || isBlocking || isAutoExport || isServerMode;
|
|
|
|
route.src = route.src.replace(
|
|
'^',
|
|
`^${dynamicPrefix ? `${dynamicPrefix}[/]?` : '[/]?'}(?${
|
|
isLocalePrefixed ? '<nextLocale>' : ':'
|
|
}${i18n.locales.map(locale => escapeStringRegexp(locale)).join('|')})?`
|
|
);
|
|
|
|
if (
|
|
isLocalePrefixed &&
|
|
!(isCorrectLocaleAPIRoutes && isApiRoute) &&
|
|
!isAppRoute
|
|
) {
|
|
// ensure destination has locale prefix to match prerender output
|
|
// path so that the prerender object is used
|
|
route.dest = route.dest!.replace(
|
|
`${path.posix.join('/', entryDirectory, '/')}`,
|
|
`${path.posix.join('/', entryDirectory, '$nextLocale', '/')}`
|
|
);
|
|
}
|
|
} else {
|
|
route.src = route.src.replace('^', `^${dynamicPrefix}`);
|
|
}
|
|
return route;
|
|
});
|
|
}
|
|
|
|
type LoaderKey = 'imgix' | 'cloudinary' | 'akamai' | 'default';
|
|
|
|
export type NextImagesManifest = {
|
|
version: number;
|
|
images: {
|
|
loader: LoaderKey;
|
|
sizes: number[];
|
|
domains: string[];
|
|
remotePatterns: Images['remotePatterns'];
|
|
minimumCacheTTL?: Images['minimumCacheTTL'];
|
|
formats?: Images['formats'];
|
|
unoptimized?: boolean;
|
|
dangerouslyAllowSVG?: Images['dangerouslyAllowSVG'];
|
|
contentSecurityPolicy?: Images['contentSecurityPolicy'];
|
|
contentDispositionType?: Images['contentDispositionType'];
|
|
};
|
|
};
|
|
|
|
export async function getImagesManifest(
|
|
entryPath: string,
|
|
outputDirectory: string
|
|
): Promise<NextImagesManifest | undefined> {
|
|
const pathImagesManifest = path.join(
|
|
entryPath,
|
|
outputDirectory,
|
|
'images-manifest.json'
|
|
);
|
|
|
|
const hasImagesManifest = await fs
|
|
.access(pathImagesManifest)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (!hasImagesManifest) {
|
|
return undefined;
|
|
}
|
|
|
|
return fs.readJson(pathImagesManifest);
|
|
}
|
|
|
|
type FileMap = { [page: string]: FileFsRef };
|
|
|
|
export function filterStaticPages(
|
|
staticPageFiles: FileMap,
|
|
dynamicPages: string[],
|
|
entryDirectory: string,
|
|
htmlContentType: string,
|
|
prerenderManifest: NextPrerenderedRoutes,
|
|
routesManifest?: RoutesManifest
|
|
) {
|
|
const staticPages: FileMap = {};
|
|
|
|
Object.keys(staticPageFiles).forEach((page: string) => {
|
|
const pathname = page.replace(/\.html$/, '');
|
|
const routeName = normalizeLocalePath(
|
|
normalizePage(pathname),
|
|
routesManifest?.i18n?.locales
|
|
).pathname;
|
|
|
|
// Prerendered routes emit a `.html` file but should not be treated as a
|
|
// static page.
|
|
// Lazily prerendered routes have a fallback `.html` file on newer
|
|
// Next.js versions so we need to also not treat it as a static page here.
|
|
if (
|
|
prerenderManifest.staticRoutes[routeName] ||
|
|
prerenderManifest.fallbackRoutes[routeName] ||
|
|
prerenderManifest.staticRoutes[normalizePage(pathname)] ||
|
|
prerenderManifest.fallbackRoutes[normalizePage(pathname)]
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const staticRoute = path.posix.join(entryDirectory, pathname);
|
|
|
|
staticPages[staticRoute] = staticPageFiles[page];
|
|
staticPages[staticRoute].contentType = htmlContentType;
|
|
|
|
if (isDynamicRoute(pathname)) {
|
|
dynamicPages.push(routeName);
|
|
return;
|
|
}
|
|
});
|
|
|
|
return staticPages;
|
|
}
|
|
|
|
export function getFilesMapFromReasons(
|
|
fileList: ReadonlySet<string>,
|
|
reasons: NodeFileTraceReasons,
|
|
ignoreFn?: (file: string, parent?: string) => boolean
|
|
): ReadonlyMap<string, Set<string>> {
|
|
// this uses the reasons tree to collect files specific to a
|
|
// certain parent allowing us to not have to trace each parent
|
|
// separately
|
|
const parentFilesMap = new Map<string, Set<string>>();
|
|
|
|
function propagateToParents(
|
|
parents: Set<string>,
|
|
file: string,
|
|
seen = new Set<string>()
|
|
) {
|
|
for (const parent of parents || []) {
|
|
if (!seen.has(parent)) {
|
|
seen.add(parent);
|
|
let parentFiles = parentFilesMap.get(parent);
|
|
|
|
if (!parentFiles) {
|
|
parentFiles = new Set();
|
|
parentFilesMap.set(parent, parentFiles);
|
|
}
|
|
|
|
if (!ignoreFn?.(file, parent)) {
|
|
parentFiles.add(file);
|
|
}
|
|
const parentReason = reasons.get(parent);
|
|
|
|
if (parentReason?.parents) {
|
|
propagateToParents(parentReason.parents, file, seen);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const file of fileList!) {
|
|
const reason = reasons!.get(file);
|
|
const isInitial =
|
|
reason?.type.length === 1 && reason.type.includes('initial');
|
|
|
|
if (
|
|
!reason ||
|
|
!reason.parents ||
|
|
(isInitial && reason.parents.size === 0)
|
|
) {
|
|
continue;
|
|
}
|
|
propagateToParents(reason.parents, file);
|
|
}
|
|
return parentFilesMap;
|
|
}
|
|
|
|
export const collectTracedFiles =
|
|
(
|
|
baseDir: string,
|
|
lstatResults: { [key: string]: ReturnType<typeof lstat> },
|
|
lstatSema: Sema,
|
|
reasons: NodeFileTraceReasons,
|
|
files: { [filePath: string]: FileFsRef }
|
|
) =>
|
|
async (file: string) => {
|
|
const reason = reasons.get(file);
|
|
if (reason && reason.type.includes('initial')) {
|
|
// Initial files are manually added to the lambda later
|
|
return;
|
|
}
|
|
const filePath = path.join(baseDir, file);
|
|
|
|
if (!lstatResults[filePath]) {
|
|
lstatResults[filePath] = lstatSema
|
|
.acquire()
|
|
.then(() => lstat(filePath))
|
|
.finally(() => lstatSema.release());
|
|
}
|
|
const { mode } = await lstatResults[filePath];
|
|
|
|
files[file] = new FileFsRef({
|
|
fsPath: path.join(baseDir, file),
|
|
mode,
|
|
});
|
|
};
|
|
|
|
export const ExperimentalTraceVersion = `9.0.4-canary.1`;
|
|
|
|
export type PseudoLayer = {
|
|
[fileName: string]: PseudoFile | PseudoSymbolicLink;
|
|
};
|
|
|
|
export type PseudoFile = {
|
|
file: FileFsRef;
|
|
isSymlink: false;
|
|
crc32: number;
|
|
uncompressedSize: number;
|
|
};
|
|
|
|
export type PseudoSymbolicLink = {
|
|
file: FileFsRef;
|
|
isSymlink: true;
|
|
symlinkTarget: string;
|
|
};
|
|
|
|
export type PseudoLayerResult = {
|
|
pseudoLayer: PseudoLayer;
|
|
pseudoLayerBytes: number;
|
|
};
|
|
|
|
export async function createPseudoLayer(files: {
|
|
[fileName: string]: FileFsRef;
|
|
}): Promise<PseudoLayerResult> {
|
|
const pseudoLayer: PseudoLayer = {};
|
|
let pseudoLayerBytes = 0;
|
|
|
|
for (const fileName of Object.keys(files)) {
|
|
const file = files[fileName];
|
|
|
|
if (isSymbolicLink(file.mode)) {
|
|
const symlinkTarget = await fs.readlink(file.fsPath);
|
|
pseudoLayer[fileName] = {
|
|
file,
|
|
isSymlink: true,
|
|
symlinkTarget,
|
|
};
|
|
} else {
|
|
const origBuffer = await streamToBuffer(file.toStream());
|
|
pseudoLayerBytes += origBuffer.byteLength;
|
|
pseudoLayer[fileName] = {
|
|
file,
|
|
isSymlink: false,
|
|
crc32: crc32.unsigned(origBuffer),
|
|
uncompressedSize: origBuffer.byteLength,
|
|
};
|
|
}
|
|
}
|
|
|
|
return { pseudoLayer, pseudoLayerBytes };
|
|
}
|
|
|
|
export interface CreateLambdaFromPseudoLayersOptions
|
|
extends LambdaOptionsWithFiles {
|
|
layers: PseudoLayer[];
|
|
isStreaming?: boolean;
|
|
nextVersion?: string;
|
|
experimentalAllowBundling?: boolean;
|
|
}
|
|
|
|
// measured with 1, 2, 5, 10, and `os.cpus().length || 5`
|
|
// and sema(1) produced the best results
|
|
const createLambdaSema = new Sema(1);
|
|
|
|
export async function createLambdaFromPseudoLayers({
|
|
files: baseFiles,
|
|
layers,
|
|
isStreaming,
|
|
nextVersion,
|
|
experimentalAllowBundling,
|
|
...lambdaOptions
|
|
}: CreateLambdaFromPseudoLayersOptions) {
|
|
await createLambdaSema.acquire();
|
|
|
|
const files: Files = {};
|
|
const addedFiles = new Set();
|
|
|
|
// Add files from pseudo layers
|
|
for (const layer of layers) {
|
|
for (const seedKey of Object.keys(layer)) {
|
|
if (addedFiles.has(seedKey)) {
|
|
// File was already added in a previous pseudo layer
|
|
continue;
|
|
}
|
|
const item = layer[seedKey];
|
|
files[seedKey] = item.file;
|
|
addedFiles.add(seedKey);
|
|
}
|
|
}
|
|
|
|
for (const fileName of Object.keys(baseFiles)) {
|
|
if (addedFiles.has(fileName)) {
|
|
// File was already added in a previous pseudo layer
|
|
continue;
|
|
}
|
|
const file = baseFiles[fileName];
|
|
files[fileName] = file;
|
|
addedFiles.add(fileName);
|
|
}
|
|
|
|
createLambdaSema.release();
|
|
|
|
return new NodejsLambda({
|
|
...lambdaOptions,
|
|
...(isStreaming
|
|
? {
|
|
supportsResponseStreaming: true,
|
|
}
|
|
: {}),
|
|
files,
|
|
shouldAddHelpers: false,
|
|
shouldAddSourcemapSupport: false,
|
|
supportsMultiPayloads: true,
|
|
framework: {
|
|
slug: 'nextjs',
|
|
version: nextVersion,
|
|
},
|
|
experimentalAllowBundling,
|
|
});
|
|
}
|
|
|
|
export type NextRequiredServerFilesManifest = {
|
|
appDir?: string;
|
|
relativeAppDir?: string;
|
|
files: string[];
|
|
ignore: string[];
|
|
config: Record<string, any>;
|
|
};
|
|
|
|
export type NextPrerenderedRoutes = {
|
|
bypassToken: string | null;
|
|
|
|
staticRoutes: {
|
|
[route: string]: {
|
|
initialRevalidate: number | false;
|
|
dataRoute: string | null;
|
|
prefetchDataRoute?: string | null;
|
|
srcRoute: string | null;
|
|
initialStatus?: number;
|
|
initialHeaders?: Record<string, string>;
|
|
experimentalBypassFor?: HasField;
|
|
experimentalPPR?: boolean;
|
|
};
|
|
};
|
|
|
|
blockingFallbackRoutes: {
|
|
[route: string]: {
|
|
routeRegex: string;
|
|
dataRoute: string | null;
|
|
dataRouteRegex: string | null;
|
|
prefetchDataRoute?: string | null;
|
|
prefetchDataRouteRegex?: string | null;
|
|
experimentalBypassFor?: HasField;
|
|
experimentalPPR?: boolean;
|
|
};
|
|
};
|
|
|
|
fallbackRoutes: {
|
|
[route: string]: {
|
|
fallback: string;
|
|
routeRegex: string;
|
|
dataRoute: string | null;
|
|
dataRouteRegex: string | null;
|
|
prefetchDataRoute?: string | null;
|
|
prefetchDataRouteRegex?: string | null;
|
|
experimentalBypassFor?: HasField;
|
|
experimentalPPR?: boolean;
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Routes that have their fallback behavior is disabled. All routes would've
|
|
* been provided in the top-level `routes` key (`staticRoutes`).
|
|
*/
|
|
omittedRoutes: {
|
|
[route: string]: {
|
|
routeRegex: string;
|
|
dataRoute: string | null;
|
|
dataRouteRegex: string | null;
|
|
prefetchDataRoute: string | null | undefined;
|
|
prefetchDataRouteRegex: string | null | undefined;
|
|
experimentalBypassFor?: HasField;
|
|
experimentalPPR?: boolean;
|
|
};
|
|
};
|
|
|
|
notFoundRoutes: string[];
|
|
|
|
isLocalePrefixed: boolean;
|
|
};
|
|
|
|
export async function getExportIntent(
|
|
entryPath: string
|
|
): Promise<false | { trailingSlash: boolean }> {
|
|
const pathExportMarker = path.join(entryPath, '.next', 'export-marker.json');
|
|
const hasExportMarker: boolean = await fs
|
|
.access(pathExportMarker, fs.constants.F_OK)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (!hasExportMarker) {
|
|
return false;
|
|
}
|
|
|
|
const manifest: {
|
|
version: 1;
|
|
exportTrailingSlash: boolean;
|
|
hasExportPathMap: boolean;
|
|
} = JSON.parse(await fs.readFile(pathExportMarker, 'utf8'));
|
|
|
|
switch (manifest.version) {
|
|
case 1: {
|
|
if (manifest.hasExportPathMap !== true) {
|
|
return false;
|
|
}
|
|
|
|
return { trailingSlash: manifest.exportTrailingSlash };
|
|
}
|
|
|
|
default: {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function getExportStatus(
|
|
entryPath: string
|
|
): Promise<false | { success: boolean; outDirectory: string }> {
|
|
const pathExportDetail = path.join(entryPath, '.next', 'export-detail.json');
|
|
const hasExportDetail: boolean = await fs
|
|
.access(pathExportDetail, fs.constants.F_OK)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (!hasExportDetail) {
|
|
return false;
|
|
}
|
|
|
|
const manifest: {
|
|
version: 1;
|
|
success: boolean;
|
|
outDirectory: string;
|
|
} = JSON.parse(await fs.readFile(pathExportDetail, 'utf8'));
|
|
|
|
switch (manifest.version) {
|
|
case 1: {
|
|
return {
|
|
success: !!manifest.success,
|
|
outDirectory: manifest.outDirectory,
|
|
};
|
|
}
|
|
|
|
default: {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function getRequiredServerFilesManifest(
|
|
entryPath: string,
|
|
outputDirectory: string
|
|
): Promise<NextRequiredServerFilesManifest | false> {
|
|
const pathRequiredServerFilesManifest = path.join(
|
|
entryPath,
|
|
outputDirectory,
|
|
'required-server-files.json'
|
|
);
|
|
|
|
const hasManifest: boolean = await fs
|
|
.access(pathRequiredServerFilesManifest, fs.constants.F_OK)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (!hasManifest) {
|
|
return false;
|
|
}
|
|
|
|
const manifestData = JSON.parse(
|
|
await fs.readFile(pathRequiredServerFilesManifest, 'utf8')
|
|
);
|
|
|
|
const requiredServerFiles = {
|
|
files: [],
|
|
ignore: [],
|
|
config: {},
|
|
appDir: manifestData.appDir,
|
|
relativeAppDir: manifestData.relativeAppDir,
|
|
};
|
|
|
|
switch (manifestData.version) {
|
|
case 1: {
|
|
requiredServerFiles.files = manifestData.files;
|
|
requiredServerFiles.ignore = manifestData.ignore;
|
|
requiredServerFiles.config = manifestData.config;
|
|
requiredServerFiles.appDir = manifestData.appDir;
|
|
break;
|
|
}
|
|
default: {
|
|
throw new Error(
|
|
`Invalid required-server-files manifest version ${manifestData.version}, please contact support if this error persists`
|
|
);
|
|
}
|
|
}
|
|
return requiredServerFiles;
|
|
}
|
|
|
|
export async function getPrerenderManifest(
|
|
entryPath: string,
|
|
outputDirectory: string
|
|
): Promise<NextPrerenderedRoutes> {
|
|
const pathPrerenderManifest = path.join(
|
|
entryPath,
|
|
outputDirectory,
|
|
'prerender-manifest.json'
|
|
);
|
|
|
|
const hasManifest: boolean = await fs
|
|
.access(pathPrerenderManifest, fs.constants.F_OK)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (!hasManifest) {
|
|
return {
|
|
staticRoutes: {},
|
|
blockingFallbackRoutes: {},
|
|
fallbackRoutes: {},
|
|
bypassToken: null,
|
|
omittedRoutes: {},
|
|
notFoundRoutes: [],
|
|
isLocalePrefixed: false,
|
|
};
|
|
}
|
|
|
|
const manifest:
|
|
| {
|
|
version: 1;
|
|
routes: {
|
|
[key: string]: {
|
|
initialRevalidateSeconds: number | false;
|
|
dataRoute: string;
|
|
srcRoute: string | null;
|
|
};
|
|
};
|
|
dynamicRoutes: {
|
|
[key: string]: {
|
|
fallback?: string;
|
|
routeRegex: string;
|
|
dataRoute: string;
|
|
dataRouteRegex: string;
|
|
};
|
|
};
|
|
preview?: {
|
|
previewModeId: string;
|
|
};
|
|
}
|
|
| {
|
|
version: 2 | 3;
|
|
routes: {
|
|
[route: string]: {
|
|
initialRevalidateSeconds: number | false;
|
|
srcRoute: string | null;
|
|
dataRoute: string;
|
|
};
|
|
};
|
|
dynamicRoutes: {
|
|
[route: string]: {
|
|
routeRegex: string;
|
|
fallback: string | false;
|
|
dataRoute: string;
|
|
dataRouteRegex: string;
|
|
};
|
|
};
|
|
preview: {
|
|
previewModeId: string;
|
|
};
|
|
notFoundRoutes?: string[];
|
|
}
|
|
| {
|
|
version: 4;
|
|
routes: {
|
|
[route: string]: {
|
|
initialRevalidateSeconds: number | false;
|
|
srcRoute: string | null;
|
|
dataRoute: string | null;
|
|
prefetchDataRoute: string | null | undefined;
|
|
initialStatus?: number;
|
|
initialHeaders?: Record<string, string>;
|
|
experimentalBypassFor?: HasField;
|
|
experimentalPPR?: boolean;
|
|
};
|
|
};
|
|
dynamicRoutes: {
|
|
[route: string]: {
|
|
routeRegex: string;
|
|
fallback: string | false;
|
|
dataRoute: string | null;
|
|
dataRouteRegex: string | null;
|
|
prefetchDataRoute: string | null | undefined;
|
|
prefetchDataRouteRegex: string | null | undefined;
|
|
experimentalBypassFor?: HasField;
|
|
experimentalPPR?: boolean;
|
|
};
|
|
};
|
|
preview: {
|
|
previewModeId: string;
|
|
};
|
|
notFoundRoutes?: string[];
|
|
} = JSON.parse(await fs.readFile(pathPrerenderManifest, 'utf8'));
|
|
|
|
switch (manifest.version) {
|
|
case 1: {
|
|
const routes = Object.keys(manifest.routes);
|
|
const lazyRoutes = Object.keys(manifest.dynamicRoutes);
|
|
|
|
const ret: NextPrerenderedRoutes = {
|
|
staticRoutes: {},
|
|
blockingFallbackRoutes: {},
|
|
fallbackRoutes: {},
|
|
bypassToken:
|
|
(manifest.preview && manifest.preview.previewModeId) || null,
|
|
omittedRoutes: {},
|
|
notFoundRoutes: [],
|
|
isLocalePrefixed: false,
|
|
};
|
|
|
|
routes.forEach(route => {
|
|
const { initialRevalidateSeconds, dataRoute, srcRoute } =
|
|
manifest.routes[route];
|
|
ret.staticRoutes[route] = {
|
|
initialRevalidate:
|
|
initialRevalidateSeconds === false
|
|
? false
|
|
: Math.max(1, initialRevalidateSeconds),
|
|
dataRoute,
|
|
srcRoute,
|
|
};
|
|
});
|
|
|
|
lazyRoutes.forEach(lazyRoute => {
|
|
const { routeRegex, fallback, dataRoute, dataRouteRegex } =
|
|
manifest.dynamicRoutes[lazyRoute];
|
|
|
|
if (fallback) {
|
|
ret.fallbackRoutes[lazyRoute] = {
|
|
routeRegex,
|
|
fallback,
|
|
dataRoute,
|
|
dataRouteRegex,
|
|
};
|
|
} else {
|
|
ret.blockingFallbackRoutes[lazyRoute] = {
|
|
routeRegex,
|
|
dataRoute,
|
|
dataRouteRegex,
|
|
};
|
|
}
|
|
});
|
|
|
|
return ret;
|
|
}
|
|
case 2:
|
|
case 3:
|
|
case 4: {
|
|
const routes = Object.keys(manifest.routes);
|
|
const lazyRoutes = Object.keys(manifest.dynamicRoutes);
|
|
|
|
const ret: NextPrerenderedRoutes = {
|
|
staticRoutes: {},
|
|
blockingFallbackRoutes: {},
|
|
fallbackRoutes: {},
|
|
bypassToken: manifest.preview.previewModeId,
|
|
omittedRoutes: {},
|
|
notFoundRoutes: [],
|
|
isLocalePrefixed: manifest.version > 2,
|
|
};
|
|
|
|
if (manifest.notFoundRoutes) {
|
|
ret.notFoundRoutes.push(...manifest.notFoundRoutes);
|
|
}
|
|
|
|
routes.forEach(route => {
|
|
const { initialRevalidateSeconds, dataRoute, srcRoute } =
|
|
manifest.routes[route];
|
|
|
|
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] = {
|
|
initialRevalidate:
|
|
initialRevalidateSeconds === false
|
|
? false
|
|
: Math.max(1, initialRevalidateSeconds),
|
|
dataRoute,
|
|
prefetchDataRoute,
|
|
srcRoute,
|
|
initialStatus,
|
|
initialHeaders,
|
|
experimentalBypassFor,
|
|
experimentalPPR,
|
|
};
|
|
});
|
|
|
|
lazyRoutes.forEach(lazyRoute => {
|
|
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 {
|
|
ret.omittedRoutes[lazyRoute] = {
|
|
experimentalBypassFor,
|
|
experimentalPPR,
|
|
routeRegex,
|
|
dataRoute,
|
|
dataRouteRegex,
|
|
prefetchDataRoute,
|
|
prefetchDataRouteRegex,
|
|
};
|
|
}
|
|
});
|
|
|
|
return ret;
|
|
}
|
|
default: {
|
|
return {
|
|
staticRoutes: {},
|
|
blockingFallbackRoutes: {},
|
|
fallbackRoutes: {},
|
|
bypassToken: null,
|
|
omittedRoutes: {},
|
|
notFoundRoutes: [],
|
|
isLocalePrefixed: false,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// We only need this once per build
|
|
let _usesSrcCache: boolean | undefined;
|
|
|
|
async function usesSrcDirectory(workPath: string): Promise<boolean> {
|
|
if (!_usesSrcCache) {
|
|
const sourcePages = path.join(workPath, 'src', 'pages');
|
|
|
|
try {
|
|
if ((await fs.stat(sourcePages)).isDirectory()) {
|
|
_usesSrcCache = true;
|
|
}
|
|
} catch (_err) {
|
|
_usesSrcCache = false;
|
|
}
|
|
}
|
|
|
|
if (!_usesSrcCache) {
|
|
const sourceAppdir = path.join(workPath, 'src', 'app');
|
|
|
|
try {
|
|
if ((await fs.stat(sourceAppdir)).isDirectory()) {
|
|
_usesSrcCache = true;
|
|
}
|
|
} catch (_err) {
|
|
_usesSrcCache = false;
|
|
}
|
|
}
|
|
|
|
return Boolean(_usesSrcCache);
|
|
}
|
|
|
|
async function getSourceFilePathFromPage({
|
|
workPath,
|
|
page,
|
|
pageExtensions,
|
|
}: {
|
|
workPath: string;
|
|
page: string;
|
|
pageExtensions?: ReadonlyArray<string>;
|
|
}) {
|
|
const usesSrcDir = await usesSrcDirectory(workPath);
|
|
const extensionsToTry = pageExtensions || ['js', 'jsx', 'ts', 'tsx'];
|
|
|
|
for (const pageType of ['pages', 'app']) {
|
|
let fsPath = path.join(workPath, pageType, page);
|
|
if (usesSrcDir) {
|
|
fsPath = path.join(workPath, 'src', pageType, page);
|
|
}
|
|
|
|
if (fs.existsSync(fsPath)) {
|
|
return path.relative(workPath, fsPath);
|
|
}
|
|
const extensionless = fsPath.replace(path.extname(fsPath), '');
|
|
|
|
for (const ext of extensionsToTry) {
|
|
fsPath = `${extensionless}.${ext}`;
|
|
// for appDir, we need to treat "index.js" as root-level "page.js"
|
|
if (
|
|
pageType === 'app' &&
|
|
extensionless ===
|
|
path.join(workPath, `${usesSrcDir ? 'src/' : ''}app/index`)
|
|
) {
|
|
fsPath = `${extensionless.replace(/index$/, 'page')}.${ext}`;
|
|
}
|
|
if (fs.existsSync(fsPath)) {
|
|
return path.relative(workPath, fsPath);
|
|
}
|
|
}
|
|
|
|
if (isDirectory(extensionless)) {
|
|
if (pageType === 'pages') {
|
|
for (const ext of extensionsToTry) {
|
|
fsPath = path.join(extensionless, `index.${ext}`);
|
|
if (fs.existsSync(fsPath)) {
|
|
return path.relative(workPath, fsPath);
|
|
}
|
|
}
|
|
// appDir
|
|
} else {
|
|
for (const ext of extensionsToTry) {
|
|
// RSC
|
|
fsPath = path.join(extensionless, `page.${ext}`);
|
|
if (fs.existsSync(fsPath)) {
|
|
return path.relative(workPath, fsPath);
|
|
}
|
|
// Route Handlers
|
|
fsPath = path.join(extensionless, `route.${ext}`);
|
|
if (fs.existsSync(fsPath)) {
|
|
return path.relative(workPath, fsPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// if we got here, and didn't find a source not-found file, then it was the one injected
|
|
// by Next.js. There's no need to warn or return a source file in this case, as it won't have
|
|
// any configuration applied to it.
|
|
if (page === '/_not-found/page') {
|
|
return '';
|
|
}
|
|
|
|
console.log(
|
|
`WARNING: Unable to find source file for page ${page} with extensions: ${extensionsToTry.join(
|
|
', '
|
|
)}, this can cause functions config from \`vercel.json\` to not be applied`
|
|
);
|
|
return '';
|
|
}
|
|
|
|
function isDirectory(path: string) {
|
|
return fs.existsSync(path) && fs.lstatSync(path).isDirectory();
|
|
}
|
|
|
|
export function normalizeLocalePath(
|
|
pathname: string,
|
|
locales?: string[]
|
|
): {
|
|
detectedLocale?: string;
|
|
pathname: string;
|
|
} {
|
|
let detectedLocale: string | undefined;
|
|
// first item will be empty string from splitting at first char
|
|
const pathnameParts = pathname.split('/');
|
|
|
|
(locales || []).some(locale => {
|
|
if (pathnameParts[1].toLowerCase() === locale.toLowerCase()) {
|
|
detectedLocale = locale;
|
|
pathnameParts.splice(1, 1);
|
|
pathname = pathnameParts.join('/') || '/';
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
return {
|
|
pathname,
|
|
detectedLocale,
|
|
};
|
|
}
|
|
|
|
export function addLocaleOrDefault(
|
|
pathname: string,
|
|
routesManifest?: RoutesManifest,
|
|
locale?: string
|
|
) {
|
|
if (!routesManifest?.i18n) return pathname;
|
|
if (!locale) locale = routesManifest.i18n.defaultLocale;
|
|
|
|
return locale
|
|
? `/${locale}${pathname === '/index' ? '' : pathname}`
|
|
: pathname;
|
|
}
|
|
|
|
export type LambdaGroup = {
|
|
pages: string[];
|
|
memory?: number;
|
|
maxDuration?: number;
|
|
isAppRouter?: boolean;
|
|
isAppRouteHandler?: boolean;
|
|
isStreaming?: boolean;
|
|
readonly isPrerenders: boolean;
|
|
readonly isExperimentalPPR: boolean;
|
|
isActionLambda?: boolean;
|
|
isPages?: boolean;
|
|
isApiLambda: boolean;
|
|
pseudoLayer: PseudoLayer;
|
|
pseudoLayerBytes: number;
|
|
pseudoLayerUncompressedBytes: number;
|
|
};
|
|
|
|
export async function getPageLambdaGroups({
|
|
entryPath,
|
|
config,
|
|
functionsConfigManifest,
|
|
pages,
|
|
prerenderRoutes,
|
|
experimentalPPRRoutes,
|
|
pageTraces,
|
|
compressedPages,
|
|
tracedPseudoLayer,
|
|
initialPseudoLayer,
|
|
initialPseudoLayerUncompressed,
|
|
internalPages,
|
|
pageExtensions,
|
|
inversedAppPathManifest,
|
|
experimentalAllowBundling,
|
|
}: {
|
|
entryPath: string;
|
|
config: Config;
|
|
functionsConfigManifest?: FunctionsConfigManifestV1;
|
|
pages: ReadonlyArray<string>;
|
|
prerenderRoutes: ReadonlySet<string>;
|
|
experimentalPPRRoutes: ReadonlySet<string> | undefined;
|
|
pageTraces: {
|
|
[page: string]: {
|
|
[key: string]: FileFsRef;
|
|
};
|
|
};
|
|
compressedPages: {
|
|
[page: string]: PseudoFile;
|
|
};
|
|
tracedPseudoLayer: PseudoLayer;
|
|
initialPseudoLayer: PseudoLayerResult;
|
|
initialPseudoLayerUncompressed: number;
|
|
internalPages: ReadonlyArray<string>;
|
|
pageExtensions?: ReadonlyArray<string>;
|
|
inversedAppPathManifest?: Record<string, string>;
|
|
experimentalAllowBundling?: boolean;
|
|
}) {
|
|
const groups: Array<LambdaGroup> = [];
|
|
|
|
for (const page of pages) {
|
|
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 } = {};
|
|
|
|
if (
|
|
functionsConfigManifest &&
|
|
functionsConfigManifest.functions[routeName]
|
|
) {
|
|
opts = functionsConfigManifest.functions[routeName];
|
|
}
|
|
|
|
if (config && config.functions) {
|
|
const sourceFile = await getSourceFilePathFromPage({
|
|
workPath: entryPath,
|
|
page: normalizeSourceFilePageFromManifest(
|
|
routeName,
|
|
page,
|
|
inversedAppPathManifest
|
|
),
|
|
pageExtensions,
|
|
});
|
|
|
|
const vercelConfigOpts = await getLambdaOptionsFromFunction({
|
|
sourceFile,
|
|
config,
|
|
});
|
|
|
|
opts = { ...vercelConfigOpts, ...opts };
|
|
}
|
|
|
|
let matchingGroup = experimentalAllowBundling
|
|
? undefined
|
|
: groups.find(group => {
|
|
const matches =
|
|
group.maxDuration === opts.maxDuration &&
|
|
group.memory === opts.memory &&
|
|
group.isPrerenders === isPrerenderRoute &&
|
|
group.isExperimentalPPR === isExperimentalPPR;
|
|
|
|
if (matches) {
|
|
let newTracedFilesUncompressedSize =
|
|
group.pseudoLayerUncompressedBytes;
|
|
|
|
for (const newPage of newPages) {
|
|
Object.keys(pageTraces[newPage] || {}).map(file => {
|
|
if (!group.pseudoLayer[file]) {
|
|
const item = tracedPseudoLayer[file] as PseudoFile;
|
|
|
|
newTracedFilesUncompressedSize += item.uncompressedSize || 0;
|
|
}
|
|
});
|
|
newTracedFilesUncompressedSize +=
|
|
compressedPages[newPage].uncompressedSize;
|
|
}
|
|
|
|
const underUncompressedLimit =
|
|
newTracedFilesUncompressedSize <
|
|
MAX_UNCOMPRESSED_LAMBDA_SIZE - LAMBDA_RESERVED_UNCOMPRESSED_SIZE;
|
|
|
|
return underUncompressedLimit;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (matchingGroup) {
|
|
matchingGroup.pages.push(page);
|
|
} else {
|
|
const newGroup: LambdaGroup = {
|
|
pages: [page],
|
|
...opts,
|
|
isPrerenders: isPrerenderRoute,
|
|
isExperimentalPPR,
|
|
isApiLambda: !!isApiPage(page),
|
|
pseudoLayerBytes: initialPseudoLayer.pseudoLayerBytes,
|
|
pseudoLayerUncompressedBytes: initialPseudoLayerUncompressed,
|
|
pseudoLayer: Object.assign({}, initialPseudoLayer.pseudoLayer),
|
|
};
|
|
groups.push(newGroup);
|
|
matchingGroup = newGroup;
|
|
}
|
|
|
|
for (const newPage of newPages) {
|
|
Object.keys(pageTraces[newPage] || {}).map(file => {
|
|
const pseudoItem = tracedPseudoLayer[file] as PseudoFile;
|
|
|
|
if (!matchingGroup!.pseudoLayer[file]) {
|
|
matchingGroup!.pseudoLayer[file] = pseudoItem;
|
|
matchingGroup!.pseudoLayerUncompressedBytes +=
|
|
pseudoItem.uncompressedSize || 0;
|
|
}
|
|
});
|
|
|
|
// ensure the page file itself is accounted for when grouping as
|
|
// large pages can be created that can push the group over the limit
|
|
matchingGroup!.pseudoLayerUncompressedBytes +=
|
|
compressedPages[newPage].uncompressedSize;
|
|
}
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
// `pages` are normalized without route groups (e.g., /app/(group)/page.js).
|
|
// we keep track of that mapping in `inversedAppPathManifest`
|
|
// `getSourceFilePathFromPage` needs to use the path from source to properly match the config
|
|
function normalizeSourceFilePageFromManifest(
|
|
routeName: string,
|
|
page: string,
|
|
inversedAppPathManifest?: Record<string, string>
|
|
) {
|
|
const pageFromManifest = inversedAppPathManifest?.[routeName];
|
|
if (!pageFromManifest) {
|
|
// since this function is used by both `pages` and `app`, the manifest might not be provided
|
|
// so fallback to normal behavior of just checking the `page`.
|
|
return page;
|
|
}
|
|
|
|
const metadataConventions = [
|
|
'/favicon.',
|
|
'/icon.',
|
|
'/apple-icon.',
|
|
'/opengraph-image.',
|
|
'/twitter-image.',
|
|
'/sitemap.',
|
|
'/robots.',
|
|
];
|
|
|
|
// these special metadata files for will not contain `/route` or `/page` suffix, so return the routeName as-is.
|
|
const isSpecialFile = metadataConventions.some(convention =>
|
|
routeName.startsWith(convention)
|
|
);
|
|
|
|
if (isSpecialFile) {
|
|
return routeName;
|
|
}
|
|
|
|
return pageFromManifest;
|
|
}
|
|
|
|
export const outputFunctionFileSizeInfo = (
|
|
pages: string[],
|
|
pseudoLayer: PseudoLayer,
|
|
pseudoLayerUncompressedBytes: number,
|
|
compressedPages: {
|
|
[page: string]: PseudoFile;
|
|
}
|
|
) => {
|
|
const exceededLimitOutput: Array<string[]> = [];
|
|
|
|
console.log(
|
|
`Serverless Function's page${pages.length === 1 ? '' : 's'}: ${pages.join(
|
|
', '
|
|
)}`
|
|
);
|
|
exceededLimitOutput.push(['Large Dependencies', 'Uncompressed size']);
|
|
|
|
const dependencies: {
|
|
[key: string]: {
|
|
uncompressed: number;
|
|
};
|
|
} = {};
|
|
|
|
for (const fileKey of Object.keys(pseudoLayer)) {
|
|
if (!pseudoLayer[fileKey].isSymlink) {
|
|
const fileItem = pseudoLayer[fileKey] as PseudoFile;
|
|
const depKey = fileKey.split('/').slice(0, 3).join('/');
|
|
|
|
if (!dependencies[depKey]) {
|
|
dependencies[depKey] = {
|
|
uncompressed: 0,
|
|
};
|
|
}
|
|
|
|
dependencies[depKey].uncompressed += fileItem.uncompressedSize;
|
|
}
|
|
}
|
|
|
|
for (const page of pages) {
|
|
dependencies[`pages/${page}`] = {
|
|
uncompressed: compressedPages[page].uncompressedSize,
|
|
};
|
|
}
|
|
let numLargeDependencies = 0;
|
|
|
|
Object.keys(dependencies)
|
|
.sort((a, b) => {
|
|
// move largest dependencies to the top
|
|
const aDep = dependencies[a];
|
|
const bDep = dependencies[b];
|
|
|
|
if (aDep.uncompressed > bDep.uncompressed) {
|
|
return -1;
|
|
}
|
|
if (aDep.uncompressed < bDep.uncompressed) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
})
|
|
.forEach(depKey => {
|
|
const dep = dependencies[depKey];
|
|
|
|
if (dep.uncompressed < 500 * KIB) {
|
|
// ignore smaller dependencies to reduce noise
|
|
return;
|
|
}
|
|
exceededLimitOutput.push([depKey, prettyBytes(dep.uncompressed)]);
|
|
numLargeDependencies += 1;
|
|
});
|
|
|
|
if (numLargeDependencies === 0) {
|
|
exceededLimitOutput.push([
|
|
'No large dependencies found (> 500KB compressed)',
|
|
]);
|
|
}
|
|
|
|
exceededLimitOutput.push([]);
|
|
exceededLimitOutput.push([
|
|
'All dependencies',
|
|
prettyBytes(pseudoLayerUncompressedBytes),
|
|
]);
|
|
|
|
console.log(
|
|
textTable(exceededLimitOutput, {
|
|
align: ['l', 'r'],
|
|
})
|
|
);
|
|
};
|
|
|
|
export const detectLambdaLimitExceeding = async (
|
|
lambdaGroups: LambdaGroup[],
|
|
compressedPages: {
|
|
[page: string]: PseudoFile;
|
|
}
|
|
) => {
|
|
// show debug info if within 5 MB of exceeding the limit
|
|
const UNCOMPRESSED_SIZE_LIMIT_CLOSE = MAX_UNCOMPRESSED_LAMBDA_SIZE - 5 * MIB;
|
|
|
|
let numExceededLimit = 0;
|
|
let numCloseToLimit = 0;
|
|
let loggedHeadInfo = false;
|
|
|
|
// pre-iterate to see if we are going to exceed the limit
|
|
// or only get close so our first log line can be correct
|
|
const filteredGroups = lambdaGroups.filter(group => {
|
|
const exceededLimit =
|
|
group.pseudoLayerUncompressedBytes > MAX_UNCOMPRESSED_LAMBDA_SIZE;
|
|
|
|
const closeToLimit =
|
|
group.pseudoLayerUncompressedBytes > UNCOMPRESSED_SIZE_LIMIT_CLOSE;
|
|
|
|
if (
|
|
closeToLimit ||
|
|
exceededLimit ||
|
|
getPlatformEnv('BUILDER_DEBUG') ||
|
|
process.env.NEXT_DEBUG_FUNCTION_SIZE
|
|
) {
|
|
if (exceededLimit) {
|
|
numExceededLimit += 1;
|
|
}
|
|
if (closeToLimit) {
|
|
numCloseToLimit += 1;
|
|
}
|
|
return true;
|
|
}
|
|
});
|
|
|
|
for (const group of filteredGroups) {
|
|
if (!loggedHeadInfo) {
|
|
if (numExceededLimit || numCloseToLimit) {
|
|
console.log(
|
|
`Warning: Max serverless function size of ${prettyBytes(
|
|
MAX_UNCOMPRESSED_LAMBDA_SIZE
|
|
)} uncompressed${numExceededLimit ? '' : ' almost'} reached`
|
|
);
|
|
} else {
|
|
console.log(`Serverless function size info`);
|
|
}
|
|
loggedHeadInfo = true;
|
|
}
|
|
|
|
outputFunctionFileSizeInfo(
|
|
group.pages,
|
|
group.pseudoLayer,
|
|
group.pseudoLayerUncompressedBytes,
|
|
compressedPages
|
|
);
|
|
}
|
|
|
|
if (numExceededLimit) {
|
|
console.log(
|
|
`Max serverless function size was exceeded for ${numExceededLimit} function${
|
|
numExceededLimit === 1 ? '' : 's'
|
|
}`
|
|
);
|
|
}
|
|
};
|
|
|
|
// checks if prerender files are all static or not before creating lambdas
|
|
export const onPrerenderRouteInitial = (
|
|
prerenderManifest: NextPrerenderedRoutes,
|
|
canUsePreviewMode: boolean,
|
|
entryDirectory: string,
|
|
nonLambdaSsgPages: Set<string>,
|
|
routeKey: string,
|
|
hasPages404: boolean,
|
|
routesManifest?: RoutesManifest,
|
|
appDir?: string | null
|
|
) => {
|
|
let static404Page: string | undefined;
|
|
let static500Page: string | undefined;
|
|
|
|
// Get the route file as it'd be mounted in the builder output
|
|
const pr = prerenderManifest.staticRoutes[routeKey];
|
|
const { initialRevalidate, srcRoute, dataRoute } = pr;
|
|
const route = srcRoute || routeKey;
|
|
|
|
const isAppPathRoute = appDir && (!dataRoute || dataRoute?.endsWith('.rsc'));
|
|
|
|
const routeNoLocale = routesManifest?.i18n
|
|
? normalizeLocalePath(routeKey, routesManifest.i18n.locales).pathname
|
|
: routeKey;
|
|
|
|
// if the 404 page used getStaticProps we need to update static404Page
|
|
// since it wasn't populated from the staticPages group
|
|
if (routeNoLocale === '/404') {
|
|
static404Page = path.posix.join(entryDirectory, routeKey);
|
|
}
|
|
|
|
if (routeNoLocale === '/500') {
|
|
static500Page = path.posix.join(entryDirectory, routeKey);
|
|
}
|
|
|
|
if (
|
|
// App paths must be Prerenders to ensure Vary header is
|
|
// correctly added
|
|
!isAppPathRoute &&
|
|
initialRevalidate === false &&
|
|
(!canUsePreviewMode || (hasPages404 && routeNoLocale === '/404')) &&
|
|
!prerenderManifest.fallbackRoutes[route] &&
|
|
!prerenderManifest.blockingFallbackRoutes[route]
|
|
) {
|
|
if (
|
|
routesManifest?.i18n &&
|
|
Object.keys(prerenderManifest.staticRoutes).some(route => {
|
|
const staticRoute = prerenderManifest.staticRoutes[route];
|
|
|
|
return (
|
|
staticRoute.srcRoute === srcRoute &&
|
|
staticRoute.initialRevalidate !== false
|
|
);
|
|
})
|
|
) {
|
|
// if any locale static routes are using revalidate the page
|
|
// requires a lambda
|
|
return {
|
|
static404Page,
|
|
static500Page,
|
|
};
|
|
}
|
|
|
|
nonLambdaSsgPages.add(route === '/' ? '/index' : route);
|
|
}
|
|
|
|
return {
|
|
static404Page,
|
|
static500Page,
|
|
};
|
|
};
|
|
|
|
type OnPrerenderRouteArgs = {
|
|
appDir: string | null;
|
|
pagesDir: string;
|
|
localePrefixed404?: boolean;
|
|
static404Page?: string;
|
|
hasPages404: boolean;
|
|
entryDirectory: string;
|
|
appPathRoutesManifest?: Record<string, string>;
|
|
prerenderManifest: NextPrerenderedRoutes;
|
|
isSharedLambdas: boolean;
|
|
isServerMode: boolean;
|
|
canUsePreviewMode: boolean;
|
|
lambdas: { [key: string]: Lambda };
|
|
experimentalStreamingLambdaPaths: ReadonlyMap<string, string> | undefined;
|
|
prerenders: { [key: string]: Prerender | File };
|
|
pageLambdaMap: { [key: string]: string };
|
|
routesManifest?: RoutesManifest;
|
|
isCorrectNotFoundRoutes?: boolean;
|
|
isEmptyAllowQueryForPrendered?: boolean;
|
|
isAppPPREnabled: boolean;
|
|
};
|
|
let prerenderGroup = 1;
|
|
|
|
export const onPrerenderRoute =
|
|
(prerenderRouteArgs: OnPrerenderRouteArgs) =>
|
|
async (
|
|
routeKey: string,
|
|
{
|
|
isBlocking,
|
|
isFallback,
|
|
isOmitted,
|
|
locale,
|
|
}: {
|
|
isBlocking?: boolean;
|
|
isFallback?: boolean;
|
|
isOmitted?: boolean;
|
|
locale?: string;
|
|
}
|
|
) => {
|
|
const {
|
|
appDir,
|
|
pagesDir,
|
|
static404Page,
|
|
localePrefixed404,
|
|
entryDirectory,
|
|
prerenderManifest,
|
|
isSharedLambdas,
|
|
isServerMode,
|
|
canUsePreviewMode,
|
|
lambdas,
|
|
experimentalStreamingLambdaPaths,
|
|
prerenders,
|
|
pageLambdaMap,
|
|
routesManifest,
|
|
isCorrectNotFoundRoutes,
|
|
isEmptyAllowQueryForPrendered,
|
|
isAppPPREnabled,
|
|
} = prerenderRouteArgs;
|
|
|
|
if (isBlocking && isFallback) {
|
|
throw new NowBuildError({
|
|
code: 'NEXT_ISBLOCKING_ISFALLBACK',
|
|
message: 'invariant: isBlocking and isFallback cannot both be true',
|
|
});
|
|
}
|
|
|
|
if (isFallback && isOmitted) {
|
|
throw new NowBuildError({
|
|
code: 'NEXT_ISOMITTED_ISFALLBACK',
|
|
message: 'invariant: isOmitted and isFallback cannot both be true',
|
|
});
|
|
}
|
|
|
|
// Get the route file as it'd be mounted in the builder output
|
|
let routeFileNoExt = routeKey === '/' ? '/index' : routeKey;
|
|
let origRouteFileNoExt = routeFileNoExt;
|
|
const { isLocalePrefixed } = prerenderManifest;
|
|
|
|
if (!locale && isLocalePrefixed) {
|
|
const localePathResult = normalizeLocalePath(
|
|
routeKey,
|
|
routesManifest?.i18n?.locales || []
|
|
);
|
|
|
|
locale = localePathResult.detectedLocale;
|
|
origRouteFileNoExt =
|
|
localePathResult.pathname === '/'
|
|
? '/index'
|
|
: localePathResult.pathname;
|
|
}
|
|
|
|
const nonDynamicSsg =
|
|
!isFallback &&
|
|
!isBlocking &&
|
|
!isOmitted &&
|
|
!prerenderManifest.staticRoutes[routeKey].srcRoute;
|
|
|
|
// if there isn't a srcRoute then it's a non-dynamic SSG page
|
|
if ((nonDynamicSsg && !isLocalePrefixed) || isFallback || isOmitted) {
|
|
routeFileNoExt = addLocaleOrDefault(
|
|
// root index files are located without folder/index.html
|
|
routeFileNoExt,
|
|
routesManifest,
|
|
locale
|
|
);
|
|
}
|
|
|
|
const isNotFound = prerenderManifest.notFoundRoutes.includes(routeKey);
|
|
|
|
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
|
|
? prerenderManifest.fallbackRoutes[routeKey]
|
|
: prerenderManifest.blockingFallbackRoutes[routeKey];
|
|
initialRevalidate = 1; // TODO: should Next.js provide this default?
|
|
// @ts-ignore
|
|
if (initialRevalidate === false) {
|
|
// Lazy routes cannot be "snapshotted" in time.
|
|
throw new NowBuildError({
|
|
code: 'NEXT_ISLAZY_INITIALREVALIDATE',
|
|
message: 'invariant isLazy: initialRevalidate !== false',
|
|
});
|
|
}
|
|
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];
|
|
({
|
|
initialRevalidate,
|
|
srcRoute,
|
|
dataRoute,
|
|
initialHeaders,
|
|
initialStatus,
|
|
experimentalBypassFor,
|
|
experimentalPPR,
|
|
prefetchDataRoute,
|
|
} = pr);
|
|
}
|
|
|
|
let isAppPathRoute = false;
|
|
|
|
// experimentalPPR signals app path route
|
|
if (appDir && experimentalPPR) {
|
|
isAppPathRoute = true;
|
|
}
|
|
|
|
// TODO: leverage manifest to determine app paths more accurately
|
|
if (appDir && srcRoute && (!dataRoute || dataRoute?.endsWith('.rsc'))) {
|
|
isAppPathRoute = true;
|
|
}
|
|
|
|
const isOmittedOrNotFound = isOmitted || isNotFound;
|
|
let htmlFsRef: File | null = null;
|
|
|
|
// 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 &&
|
|
!prefetchDataRoute &&
|
|
isAppPathRoute &&
|
|
!(isBlocking || isFallback)
|
|
) {
|
|
const contentType = initialHeaders?.['content-type'];
|
|
|
|
// If the route has a body file, use it as the fallback, otherwise it may
|
|
// not have an associated fallback. This could be the case for routes that
|
|
// have dynamic segments.
|
|
const fsPath = path.join(appDir, `${routeFileNoExt}.body`);
|
|
if (fs.existsSync(fsPath)) {
|
|
htmlFsRef = new FileFsRef({
|
|
fsPath,
|
|
contentType: contentType || 'text/html;charset=utf-8',
|
|
});
|
|
}
|
|
} else {
|
|
htmlFsRef =
|
|
isBlocking || (isNotFound && !static404Page)
|
|
? // Blocking pages do not have an HTML fallback
|
|
null
|
|
: new FileFsRef({
|
|
fsPath: path.join(
|
|
isAppPathRoute && !isOmittedOrNotFound && appDir
|
|
? appDir
|
|
: pagesDir,
|
|
isFallback
|
|
? // Fallback pages have a special file.
|
|
addLocaleOrDefault(
|
|
prerenderManifest.fallbackRoutes[routeKey].fallback,
|
|
routesManifest,
|
|
locale
|
|
)
|
|
: // Otherwise, the route itself should exist as a static HTML
|
|
// file.
|
|
`${
|
|
isOmittedOrNotFound
|
|
? localePrefixed404
|
|
? addLocaleOrDefault('/404', routesManifest, locale)
|
|
: '/404'
|
|
: routeFileNoExt
|
|
}.html`
|
|
),
|
|
});
|
|
}
|
|
const jsonFsRef =
|
|
// JSON data does not exist for fallback or blocking pages
|
|
isFallback || isBlocking || (isNotFound && !static404Page) || !dataRoute
|
|
? null
|
|
: new FileFsRef({
|
|
fsPath: path.join(
|
|
isAppPathRoute && !isOmittedOrNotFound && appDir
|
|
? appDir
|
|
: pagesDir,
|
|
`${
|
|
isOmittedOrNotFound
|
|
? localePrefixed404
|
|
? addLocaleOrDefault('/404.html', routesManifest, locale)
|
|
: '/404.html'
|
|
: isAppPathRoute
|
|
? prefetchDataRoute || dataRoute
|
|
: routeFileNoExt + '.json'
|
|
}`
|
|
),
|
|
});
|
|
|
|
if (isOmittedOrNotFound) {
|
|
initialStatus = 404;
|
|
}
|
|
|
|
let outputPathPage = path.posix.join(entryDirectory, routeFileNoExt);
|
|
|
|
if (!isAppPathRoute) {
|
|
outputPathPage = normalizeIndexOutput(outputPathPage, isServerMode);
|
|
}
|
|
|
|
const outputPathPageOrig = path.posix.join(
|
|
entryDirectory,
|
|
origRouteFileNoExt
|
|
);
|
|
|
|
let lambda: undefined | Lambda;
|
|
|
|
function normalizeDataRoute(route: string) {
|
|
let normalized = path.posix.join(entryDirectory, route);
|
|
|
|
if (nonDynamicSsg || isFallback || isOmitted) {
|
|
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
|
|
// replace on the routeFile and then the second on the outputPath
|
|
`${routeFileNoExt.replace(/\$/g, '$$$$')}.json`
|
|
);
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
let outputPathData: null | string = null;
|
|
if (dataRoute) {
|
|
outputPathData = normalizeDataRoute(dataRoute);
|
|
}
|
|
|
|
let outputPathPrefetchData: null | string = null;
|
|
if (prefetchDataRoute) {
|
|
if (!isAppPPREnabled) {
|
|
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(
|
|
'/',
|
|
srcRoute == null
|
|
? outputPathPageOrig
|
|
: path.posix.join(
|
|
entryDirectory,
|
|
srcRoute === '/' ? '/index' : srcRoute
|
|
)
|
|
),
|
|
isServerMode
|
|
);
|
|
|
|
const lambdaId = pageLambdaMap[outputSrcPathPage];
|
|
lambda = lambdas[lambdaId];
|
|
} else {
|
|
let outputSrcPathPage =
|
|
srcRoute == null
|
|
? outputPathPageOrig
|
|
: path.posix.join(
|
|
entryDirectory,
|
|
srcRoute === '/' ? '/index' : srcRoute
|
|
);
|
|
|
|
if (!isAppPathRoute) {
|
|
outputSrcPathPage = normalizeIndexOutput(
|
|
outputSrcPathPage,
|
|
isServerMode
|
|
);
|
|
}
|
|
|
|
lambda = lambdas[outputSrcPathPage];
|
|
}
|
|
|
|
if (!isAppPathRoute && !isNotFound && initialRevalidate === false) {
|
|
if (htmlFsRef == null || jsonFsRef == null) {
|
|
throw new NowBuildError({
|
|
code: 'NEXT_HTMLFSREF_JSONFSREF',
|
|
message: `invariant: htmlFsRef != null && jsonFsRef != null ${routeFileNoExt}`,
|
|
});
|
|
}
|
|
|
|
// if preview mode/On-Demand ISR can't be leveraged
|
|
// we can output pure static outputs instead of prerenders
|
|
if (
|
|
!canUsePreviewMode ||
|
|
(routeKey === '/404' && !lambdas[outputPathPage])
|
|
) {
|
|
htmlFsRef.contentType = htmlContentType;
|
|
prerenders[outputPathPage] = htmlFsRef;
|
|
|
|
if (outputPrerenderPathData) {
|
|
prerenders[outputPrerenderPathData] = jsonFsRef;
|
|
}
|
|
}
|
|
}
|
|
const isNotFoundPreview =
|
|
isCorrectNotFoundRoutes &&
|
|
!initialRevalidate &&
|
|
canUsePreviewMode &&
|
|
isServerMode &&
|
|
isNotFound;
|
|
|
|
if (
|
|
prerenders[outputPathPage] == null &&
|
|
(!isNotFound || initialRevalidate || isNotFoundPreview)
|
|
) {
|
|
if (lambda == null) {
|
|
throw new NowBuildError({
|
|
code: 'NEXT_MISSING_LAMBDA',
|
|
message: `Unable to find lambda for route: ${routeFileNoExt}`,
|
|
});
|
|
}
|
|
|
|
// `allowQuery` is an array of query parameter keys that are allowed for
|
|
// a given path. All other query keys will be striped. We can automatically
|
|
// detect this for prerender (ISR) pages by reading the routes manifest file.
|
|
const pageKey = srcRoute || routeKey;
|
|
const route = routesManifest?.dynamicRoutes.find(
|
|
(r): r is RoutesManifestRoute =>
|
|
r.page === pageKey && !('isMiddleware' in r)
|
|
) as RoutesManifestRoute | undefined;
|
|
const routeKeys = route?.routeKeys;
|
|
// by default allowQuery should be undefined and only set when
|
|
// we have sufficient information to set it
|
|
let allowQuery: string[] | undefined;
|
|
|
|
if (isEmptyAllowQueryForPrendered) {
|
|
const isDynamic = isDynamicRoute(routeKey);
|
|
|
|
if (!isDynamic) {
|
|
// for non-dynamic routes we use an empty array since
|
|
// no query values bust the cache for non-dynamic prerenders
|
|
// prerendered paths also do not pass allowQuery as they match
|
|
// during handle: 'filesystem' so should not cache differently
|
|
// by query values
|
|
allowQuery = [];
|
|
} else if (routeKeys) {
|
|
// if we have routeKeys in the routes-manifest we use those
|
|
// for allowQuery for dynamic routes
|
|
allowQuery = Object.values(routeKeys);
|
|
}
|
|
} else {
|
|
const isDynamic = isDynamicRoute(pageKey);
|
|
|
|
if (routeKeys) {
|
|
// if we have routeKeys in the routes-manifest we use those
|
|
// for allowQuery for dynamic routes
|
|
allowQuery = Object.values(routeKeys);
|
|
} else if (!isDynamic) {
|
|
// for non-dynamic routes we use an empty array since
|
|
// no query values bust the cache for non-dynamic prerenders
|
|
allowQuery = [];
|
|
}
|
|
}
|
|
|
|
const rscEnabled = !!routesManifest?.rsc;
|
|
const rscVaryHeader =
|
|
routesManifest?.rsc?.varyHeader ||
|
|
'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;
|
|
}
|
|
|
|
let experimentalStreamingLambdaPath: string | undefined;
|
|
if (experimentalPPR) {
|
|
if (!experimentalStreamingLambdaPaths) {
|
|
throw new Error(
|
|
"Invariant: experimentalStreamingLambdaPaths doesn't exist"
|
|
);
|
|
}
|
|
|
|
// 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)
|
|
);
|
|
if (!experimentalStreamingLambdaPath && srcRoute) {
|
|
experimentalStreamingLambdaPath =
|
|
experimentalStreamingLambdaPaths.get(
|
|
pathnameToOutputName(entryDirectory, srcRoute)
|
|
);
|
|
}
|
|
|
|
if (!experimentalStreamingLambdaPath) {
|
|
throw new Error(
|
|
`Invariant: experimentalStreamingLambdaPath is undefined for routeKey=${routeKey} and srcRoute=${
|
|
srcRoute ?? 'null'
|
|
}`
|
|
);
|
|
}
|
|
}
|
|
|
|
prerenders[outputPathPage] = new Prerender({
|
|
expiration: initialRevalidate,
|
|
lambda,
|
|
allowQuery,
|
|
fallback: htmlFsRef,
|
|
group: prerenderGroup,
|
|
bypassToken: prerenderManifest.bypassToken,
|
|
experimentalBypassFor,
|
|
initialStatus,
|
|
initialHeaders,
|
|
sourcePath,
|
|
experimentalStreamingLambdaPath,
|
|
|
|
...(isNotFound
|
|
? {
|
|
initialStatus: 404,
|
|
}
|
|
: {}),
|
|
|
|
...(rscEnabled
|
|
? {
|
|
initialHeaders: {
|
|
...initialHeaders,
|
|
vary: rscVaryHeader,
|
|
},
|
|
}
|
|
: {}),
|
|
});
|
|
|
|
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,
|
|
fallback: jsonFsRef,
|
|
group: prerenderGroup,
|
|
bypassToken: prerenderManifest.bypassToken,
|
|
experimentalBypassFor,
|
|
|
|
...(isNotFound
|
|
? {
|
|
initialStatus: 404,
|
|
}
|
|
: {}),
|
|
|
|
...(rscEnabled
|
|
? {
|
|
initialHeaders: {
|
|
...initialHeaders,
|
|
'content-type': rscContentTypeHeader,
|
|
vary: rscVaryHeader,
|
|
// If it contains a pre-render, then it was postponed.
|
|
...(prerender && rscDidPostponeHeader
|
|
? { [rscDidPostponeHeader]: '1' }
|
|
: {}),
|
|
},
|
|
}
|
|
: {}),
|
|
});
|
|
}
|
|
|
|
// we need to ensure all prerenders have a matching .rsc output
|
|
// otherwise routing could fall through unexpectedly for the
|
|
// fallback: false case as it doesn't have a dynamic route
|
|
// to catch the `.rsc` request for app -> pages routing
|
|
if (outputPrerenderPathData?.endsWith('.json') && appDir) {
|
|
const dummyOutput = new FileBlob({
|
|
data: '{}',
|
|
contentType: 'application/json',
|
|
});
|
|
const rscKey = `${outputPathPage}.rsc`;
|
|
const prefetchRscKey = `${outputPathPage}${RSC_PREFETCH_SUFFIX}`;
|
|
|
|
prerenders[rscKey] = dummyOutput;
|
|
prerenders[prefetchRscKey] = dummyOutput;
|
|
}
|
|
|
|
++prerenderGroup;
|
|
|
|
if (routesManifest?.i18n && isBlocking) {
|
|
for (const locale of routesManifest.i18n.locales) {
|
|
const localeRouteFileNoExt = addLocaleOrDefault(
|
|
routeFileNoExt,
|
|
routesManifest,
|
|
locale
|
|
);
|
|
let localeOutputPathPage = path.posix.join(
|
|
entryDirectory,
|
|
localeRouteFileNoExt
|
|
);
|
|
|
|
if (!isAppPathRoute) {
|
|
localeOutputPathPage = normalizeIndexOutput(
|
|
localeOutputPathPage,
|
|
isServerMode
|
|
);
|
|
}
|
|
|
|
const origPrerenderPage = prerenders[outputPathPage];
|
|
prerenders[localeOutputPathPage] = {
|
|
...origPrerenderPage,
|
|
group: prerenderGroup,
|
|
} as Prerender;
|
|
|
|
if (outputPathData) {
|
|
const localeOutputPathData = outputPathData.replace(
|
|
new RegExp(`${escapeStringRegexp(origRouteFileNoExt)}.json$`),
|
|
`${localeRouteFileNoExt}${
|
|
localeRouteFileNoExt !== origRouteFileNoExt &&
|
|
origRouteFileNoExt === '/index'
|
|
? '/index'
|
|
: ''
|
|
}.json`
|
|
);
|
|
const origPrerenderData = prerenders[outputPathData];
|
|
|
|
prerenders[localeOutputPathData] = {
|
|
...origPrerenderData,
|
|
group: prerenderGroup,
|
|
} as Prerender;
|
|
}
|
|
++prerenderGroup;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
((nonDynamicSsg && !isLocalePrefixed) || isFallback || isOmitted) &&
|
|
routesManifest?.i18n &&
|
|
!locale
|
|
) {
|
|
// load each locale
|
|
for (const locale of routesManifest.i18n.locales) {
|
|
if (locale === routesManifest.i18n.defaultLocale) continue;
|
|
onPrerenderRoute(prerenderRouteArgs)(routeKey, {
|
|
isBlocking,
|
|
isFallback,
|
|
isOmitted,
|
|
locale,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
export type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
|
|
|
|
export async function getStaticFiles(
|
|
entryPath: string,
|
|
entryDirectory: string,
|
|
outputDirectory: string
|
|
) {
|
|
const collectLabel =
|
|
'Collected static files (public/, static/, .next/static)';
|
|
console.time(collectLabel);
|
|
|
|
const nextStaticFiles = await glob(
|
|
'**',
|
|
path.join(entryPath, outputDirectory, 'static')
|
|
);
|
|
const staticFolderFiles = await glob('**', path.join(entryPath, 'static'));
|
|
|
|
let publicFolderFiles: UnwrapPromise<ReturnType<typeof glob>> = {};
|
|
let publicFolderPath: string | undefined;
|
|
|
|
if (await fs.pathExists(path.join(entryPath, 'public'))) {
|
|
publicFolderPath = path.join(entryPath, 'public');
|
|
} else if (
|
|
// check at the same level as the output directory also
|
|
await fs.pathExists(path.join(entryPath, outputDirectory, '../public'))
|
|
) {
|
|
publicFolderPath = path.join(entryPath, outputDirectory, '../public');
|
|
}
|
|
|
|
if (publicFolderPath) {
|
|
debug(`Using public folder at ${publicFolderPath}`);
|
|
publicFolderFiles = await glob('**/*', publicFolderPath);
|
|
} else {
|
|
debug('No public folder found');
|
|
}
|
|
const staticFiles: Record<string, FileFsRef> = {};
|
|
const staticDirectoryFiles: Record<string, FileFsRef> = {};
|
|
const publicDirectoryFiles: Record<string, FileFsRef> = {};
|
|
|
|
for (const file of Object.keys(nextStaticFiles)) {
|
|
staticFiles[path.posix.join(entryDirectory, `_next/static/${file}`)] =
|
|
nextStaticFiles[file];
|
|
}
|
|
|
|
for (const file of Object.keys(staticFolderFiles)) {
|
|
staticDirectoryFiles[path.posix.join(entryDirectory, 'static', file)] =
|
|
staticFolderFiles[file];
|
|
}
|
|
|
|
for (const file of Object.keys(publicFolderFiles)) {
|
|
publicDirectoryFiles[path.posix.join(entryDirectory, file)] =
|
|
publicFolderFiles[file];
|
|
}
|
|
|
|
console.timeEnd(collectLabel);
|
|
return {
|
|
staticFiles,
|
|
staticDirectoryFiles,
|
|
publicDirectoryFiles,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Strips the trailing `/index` from the output name if it's not the root if
|
|
* the server mode is enabled.
|
|
*/
|
|
export function normalizeIndexOutput(
|
|
outputName: string,
|
|
isServerMode: boolean
|
|
) {
|
|
if (outputName !== 'index' && outputName !== '/index' && isServerMode) {
|
|
return outputName.replace(/\/index$/, '');
|
|
}
|
|
return outputName;
|
|
}
|
|
|
|
/**
|
|
* The path to next-server was changed in
|
|
* https://github.com/vercel/next.js/pull/26756
|
|
*/
|
|
export function getNextServerPath(nextVersion: string) {
|
|
return semver.gte(nextVersion, 'v11.0.2-canary.4')
|
|
? 'next/dist/server'
|
|
: 'next/dist/next-server/server';
|
|
}
|
|
|
|
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
|
|
export function updateRouteSrc(
|
|
route: Route,
|
|
index: number,
|
|
manifestItems: Array<{ regex: string }>
|
|
) {
|
|
if (route.src) {
|
|
route.src = manifestItems[index].regex;
|
|
}
|
|
return route;
|
|
}
|
|
|
|
export async function getPrivateOutputs(
|
|
dir: string,
|
|
entries: Record<string, string>
|
|
) {
|
|
const files: Files = {};
|
|
const routes: Route[] = [];
|
|
|
|
for (const [existingFile, outputFile] of Object.entries(entries)) {
|
|
const fsPath = path.join(dir, existingFile);
|
|
|
|
try {
|
|
const { mode, size } = await stat(fsPath);
|
|
if (size > 30 * 1024 * 1024) {
|
|
throw new Error(`Exceeds maximum file size: ${size}`);
|
|
}
|
|
files[outputFile] = new FileFsRef({ mode, fsPath });
|
|
routes.push({
|
|
src: `/${outputFile}`,
|
|
dest: '/404',
|
|
status: 404,
|
|
continue: true,
|
|
});
|
|
} catch (error) {
|
|
debug(
|
|
`Private file ${existingFile} had an error and will not be uploaded: ${error}`
|
|
);
|
|
}
|
|
}
|
|
|
|
return { files, routes };
|
|
}
|
|
|
|
export {
|
|
excludeFiles,
|
|
validateEntrypoint,
|
|
normalizePackageJson,
|
|
getNextConfig,
|
|
getImagesConfig,
|
|
stringMap,
|
|
normalizePage,
|
|
isDynamicRoute,
|
|
getSourceFilePathFromPage,
|
|
};
|
|
|
|
export type FunctionsConfigManifestV1 = {
|
|
version: 1;
|
|
functions: Record<
|
|
string,
|
|
{
|
|
maxDuration?: number;
|
|
}
|
|
>;
|
|
};
|
|
|
|
type MiddlewareManifest =
|
|
| MiddlewareManifestV1
|
|
| MiddlewareManifestV2
|
|
| MiddlewareManifestV3;
|
|
|
|
interface MiddlewareManifestV1 {
|
|
version: 1;
|
|
sortedMiddleware: string[];
|
|
middleware: { [page: string]: EdgeFunctionInfoV1 };
|
|
functions?: { [page: string]: EdgeFunctionInfoV1 };
|
|
}
|
|
|
|
interface MiddlewareManifestV2 {
|
|
version: 2;
|
|
sortedMiddleware: string[];
|
|
middleware: { [page: string]: EdgeFunctionInfoV2 };
|
|
functions?: { [page: string]: EdgeFunctionInfoV2 };
|
|
}
|
|
|
|
interface MiddlewareManifestV3 {
|
|
version: 3;
|
|
sortedMiddleware: string[];
|
|
middleware: { [page: string]: EdgeFunctionInfoV3 };
|
|
functions?: { [page: string]: EdgeFunctionInfoV3 };
|
|
}
|
|
|
|
type Regions = 'home' | 'global' | 'auto' | string[] | 'all' | 'default';
|
|
|
|
interface BaseEdgeFunctionInfo {
|
|
files: string[];
|
|
name: string;
|
|
page: string;
|
|
wasm?: { filePath: string; name: string }[];
|
|
assets?: { filePath: string; name: string }[];
|
|
regions?: Regions;
|
|
}
|
|
|
|
interface EdgeFunctionInfoV1 extends BaseEdgeFunctionInfo {
|
|
regexp: string;
|
|
}
|
|
|
|
interface EdgeFunctionInfoV2 extends BaseEdgeFunctionInfo {
|
|
matchers: EdgeFunctionMatcher[];
|
|
}
|
|
|
|
interface EdgeFunctionInfoV3 extends BaseEdgeFunctionInfo {
|
|
matchers: EdgeFunctionMatcher[];
|
|
env: Record<string, string>;
|
|
}
|
|
|
|
interface EdgeFunctionMatcher {
|
|
regexp: string;
|
|
has?: HasField;
|
|
missing?: HasField;
|
|
originalSource?: string;
|
|
}
|
|
|
|
const vercelFunctionRegionsVar = process.env.VERCEL_FUNCTION_REGIONS;
|
|
let vercelFunctionRegions: string[] | undefined;
|
|
if (vercelFunctionRegionsVar) {
|
|
vercelFunctionRegions = vercelFunctionRegionsVar.split(',');
|
|
}
|
|
|
|
/**
|
|
* Normalizes the regions config that comes from the Next.js edge functions manifest.
|
|
* Ensures that config like `home` and `global` are converted to the corresponding Vercel region config.
|
|
* In the future we'll want to make `home` and `global` part of the Build Output API.
|
|
* - `home` refers to the regions set in vercel.json or on the Vercel dashboard project config.
|
|
* - `global` refers to all regions.
|
|
*/
|
|
function normalizeRegions(regions: Regions): undefined | string | string[] {
|
|
if (typeof regions === 'string') {
|
|
regions = [regions];
|
|
}
|
|
|
|
const newRegions: string[] = [];
|
|
for (const region of regions) {
|
|
// Explicitly mentioned as `home` is one of the explicit values for preferredRegion in Next.js.
|
|
if (region === 'home') {
|
|
if (vercelFunctionRegions) {
|
|
// Includes the regions from the VERCEL_FUNCTION_REGIONS env var.
|
|
newRegions.push(...vercelFunctionRegions);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Explicitly mentioned as `global` is one of the explicit values for preferredRegion in Next.js.
|
|
if (region === 'global') {
|
|
// Uses `all` instead as that's how it's implemented on Vercel.
|
|
// Returns here as when all is provided all regions will be matched.
|
|
return 'all';
|
|
}
|
|
|
|
// Explicitly mentioned as `auto` is one of the explicit values for preferredRegion in Next.js.
|
|
if (region === 'auto') {
|
|
// Returns here as when auto is provided all regions will be matched.
|
|
return 'auto';
|
|
}
|
|
|
|
newRegions.push(region);
|
|
}
|
|
|
|
// Ensure we don't pass an empty array as that is not supported.
|
|
if (newRegions.length === 0) {
|
|
return undefined;
|
|
}
|
|
|
|
return newRegions;
|
|
}
|
|
|
|
export function normalizeEdgeFunctionPath(
|
|
shortPath: string,
|
|
appPathRoutesManifest: Record<string, string>
|
|
) {
|
|
if (
|
|
shortPath.startsWith('app/') &&
|
|
(shortPath.endsWith('/page') ||
|
|
shortPath.endsWith('/route') ||
|
|
shortPath === 'app/_not-found')
|
|
) {
|
|
const ogRoute = shortPath.replace(/^app\//, '/');
|
|
shortPath = (
|
|
appPathRoutesManifest[ogRoute] ||
|
|
shortPath.replace(/(^|\/)(page|route)$/, '')
|
|
).replace(/^\//, '');
|
|
|
|
if (!shortPath || shortPath === '/') {
|
|
shortPath = 'index';
|
|
}
|
|
}
|
|
|
|
if (shortPath.startsWith('pages/')) {
|
|
shortPath = shortPath.replace(/^pages\//, '');
|
|
}
|
|
|
|
return shortPath;
|
|
}
|
|
|
|
export async function getMiddlewareBundle({
|
|
entryPath,
|
|
outputDirectory,
|
|
routesManifest,
|
|
isCorrectMiddlewareOrder,
|
|
prerenderBypassToken,
|
|
nextVersion,
|
|
appPathRoutesManifest,
|
|
}: {
|
|
config: Config;
|
|
entryPath: string;
|
|
outputDirectory: string;
|
|
prerenderBypassToken: string;
|
|
routesManifest: RoutesManifest;
|
|
isCorrectMiddlewareOrder: boolean;
|
|
nextVersion: string;
|
|
appPathRoutesManifest: Record<string, string>;
|
|
}): Promise<{
|
|
staticRoutes: Route[];
|
|
dynamicRouteMap: ReadonlyMap<string, RouteWithSrc>;
|
|
edgeFunctions: Record<string, EdgeFunction>;
|
|
}> {
|
|
const middlewareManifest = await getMiddlewareManifest(
|
|
entryPath,
|
|
outputDirectory
|
|
);
|
|
const sortedFunctions = [
|
|
...(!middlewareManifest
|
|
? []
|
|
: middlewareManifest.sortedMiddleware.map(key => ({
|
|
key,
|
|
edgeFunction: middlewareManifest?.middleware[key],
|
|
type: 'middleware' as const,
|
|
}))),
|
|
|
|
...Object.entries(middlewareManifest?.functions ?? {}).map(
|
|
([key, edgeFunction]) => {
|
|
return {
|
|
key,
|
|
edgeFunction,
|
|
type: 'function' as const,
|
|
};
|
|
}
|
|
),
|
|
];
|
|
|
|
if (middlewareManifest && sortedFunctions.length > 0) {
|
|
const workerConfigs = await Promise.all(
|
|
sortedFunctions.map(async ({ key, edgeFunction, type }) => {
|
|
try {
|
|
const wrappedModuleSource = await getNextjsEdgeFunctionSource(
|
|
edgeFunction.files,
|
|
{
|
|
name: edgeFunction.name,
|
|
staticRoutes: routesManifest.staticRoutes,
|
|
dynamicRoutes: routesManifest.dynamicRoutes.filter(
|
|
r => !('isMiddleware' in r)
|
|
),
|
|
nextConfig: {
|
|
basePath: routesManifest.basePath,
|
|
i18n: routesManifest.i18n,
|
|
},
|
|
},
|
|
path.resolve(entryPath, outputDirectory),
|
|
edgeFunction.wasm
|
|
);
|
|
|
|
return {
|
|
type,
|
|
page: edgeFunction.page,
|
|
name: edgeFunction.name,
|
|
edgeFunction: (() => {
|
|
const { source, map } = wrappedModuleSource.sourceAndMap();
|
|
const transformedMap = stringifySourceMap(
|
|
transformSourceMap(map)
|
|
);
|
|
|
|
const wasmFiles = (edgeFunction.wasm ?? []).reduce(
|
|
(acc: Files, { filePath, name }) => {
|
|
const fullFilePath = path.join(
|
|
entryPath,
|
|
outputDirectory,
|
|
filePath
|
|
);
|
|
acc[`wasm/${name}.wasm`] = new FileFsRef({
|
|
mode: 0o644,
|
|
contentType: 'application/wasm',
|
|
fsPath: fullFilePath,
|
|
});
|
|
return acc;
|
|
},
|
|
{}
|
|
);
|
|
|
|
const assetFiles = (edgeFunction.assets ?? []).reduce(
|
|
(acc: Files, { filePath, name }) => {
|
|
const fullFilePath = path.join(
|
|
entryPath,
|
|
outputDirectory,
|
|
filePath
|
|
);
|
|
acc[`assets/${name}`] = new FileFsRef({
|
|
mode: 0o644,
|
|
contentType: 'application/octet-stream',
|
|
fsPath: fullFilePath,
|
|
});
|
|
return acc;
|
|
},
|
|
{}
|
|
);
|
|
|
|
return new EdgeFunction({
|
|
deploymentTarget: 'v8-worker',
|
|
name: edgeFunction.name,
|
|
files: {
|
|
'index.js': new FileBlob({
|
|
data: source,
|
|
contentType: 'application/javascript',
|
|
mode: 0o644,
|
|
}),
|
|
...(transformedMap && {
|
|
'index.js.map': new FileBlob({
|
|
data: transformedMap,
|
|
contentType: 'application/json',
|
|
mode: 0o644,
|
|
}),
|
|
}),
|
|
...wasmFiles,
|
|
...assetFiles,
|
|
},
|
|
regions: edgeFunction.regions
|
|
? normalizeRegions(edgeFunction.regions)
|
|
: undefined,
|
|
entrypoint: 'index.js',
|
|
assets: (edgeFunction.assets ?? []).map(({ name }) => {
|
|
return {
|
|
name,
|
|
path: `assets/${name}`,
|
|
};
|
|
}),
|
|
framework: {
|
|
slug: 'nextjs',
|
|
version: nextVersion,
|
|
},
|
|
environment: edgeFunction.env,
|
|
});
|
|
})(),
|
|
routeMatchers: getRouteMatchers(edgeFunction, routesManifest),
|
|
};
|
|
} catch (e: any) {
|
|
e.message = `Can't build edge function ${key}: ${e.message}`;
|
|
throw e;
|
|
}
|
|
})
|
|
);
|
|
|
|
const source: {
|
|
staticRoutes: Route[];
|
|
dynamicRouteMap: Map<string, RouteWithSrc>;
|
|
edgeFunctions: Record<string, EdgeFunction>;
|
|
} = {
|
|
staticRoutes: [],
|
|
dynamicRouteMap: new Map(),
|
|
edgeFunctions: {},
|
|
};
|
|
|
|
for (const worker of workerConfigs.values()) {
|
|
let shortPath = worker.name;
|
|
|
|
// Replacing the folder prefix for the page
|
|
//
|
|
// For `pages/`, use file base name directly:
|
|
// pages/index -> index
|
|
// For `app/`, use folder name, handle the root page as index:
|
|
// app/route/page -> route
|
|
// app/page -> index
|
|
// app/index/page -> index/index
|
|
if (shortPath.startsWith('pages/')) {
|
|
shortPath = shortPath.replace(/^pages\//, '');
|
|
} else {
|
|
shortPath = normalizeEdgeFunctionPath(shortPath, appPathRoutesManifest);
|
|
}
|
|
|
|
if (routesManifest?.basePath) {
|
|
const isAppPathRoute = !!appPathRoutesManifest[shortPath];
|
|
|
|
shortPath = path.posix.join(
|
|
'./',
|
|
routesManifest?.basePath,
|
|
shortPath.replace(/^\//, '')
|
|
);
|
|
|
|
if (!isAppPathRoute) {
|
|
shortPath = normalizeIndexOutput(shortPath, true);
|
|
}
|
|
}
|
|
|
|
worker.edgeFunction.name = shortPath;
|
|
source.edgeFunctions[shortPath] = worker.edgeFunction;
|
|
|
|
// we don't add the route for edge functions as these
|
|
// are already added in the routes-manifest under dynamicRoutes
|
|
if (worker.type === 'function') {
|
|
continue;
|
|
}
|
|
|
|
for (const matcher of worker.routeMatchers) {
|
|
const route: Route = {
|
|
continue: true,
|
|
src: matcher.regexp,
|
|
has: matcher.has,
|
|
missing: [
|
|
{
|
|
type: 'header',
|
|
key: 'x-prerender-revalidate',
|
|
value: prerenderBypassToken,
|
|
},
|
|
...(matcher.missing || []),
|
|
],
|
|
};
|
|
|
|
route.middlewarePath = shortPath;
|
|
route.middlewareRawSrc = matcher.originalSource
|
|
? [matcher.originalSource]
|
|
: [];
|
|
if (isCorrectMiddlewareOrder) {
|
|
route.override = true;
|
|
}
|
|
|
|
if (routesManifest.version > 3 && isDynamicRoute(worker.page)) {
|
|
source.dynamicRouteMap.set(worker.page, route);
|
|
} else {
|
|
source.staticRoutes.push(route);
|
|
}
|
|
}
|
|
}
|
|
return source;
|
|
}
|
|
|
|
return {
|
|
staticRoutes: [],
|
|
dynamicRouteMap: new Map(),
|
|
edgeFunctions: {},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Attempts to read the functions config manifest from the pre-defined
|
|
* location. If the manifest can't be found it will resolve to
|
|
* undefined.
|
|
*/
|
|
export async function getFunctionsConfigManifest(
|
|
entryPath: string,
|
|
outputDirectory: string
|
|
): Promise<FunctionsConfigManifestV1 | undefined> {
|
|
const functionConfigManifestPath = path.join(
|
|
entryPath,
|
|
outputDirectory,
|
|
'./server/functions-config-manifest.json'
|
|
);
|
|
|
|
const hasManifest = await fs
|
|
.access(functionConfigManifestPath)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (!hasManifest) {
|
|
return;
|
|
}
|
|
|
|
const manifest: FunctionsConfigManifestV1 = await fs.readJSON(
|
|
functionConfigManifestPath
|
|
);
|
|
|
|
return manifest.version === 1 ? manifest : undefined;
|
|
}
|
|
|
|
/**
|
|
* Attempts to read the middleware manifest from the pre-defined
|
|
* location. If the manifest can't be found it will resolve to
|
|
* undefined.
|
|
*/
|
|
export async function getMiddlewareManifest(
|
|
entryPath: string,
|
|
outputDirectory: string
|
|
): Promise<MiddlewareManifestV3 | undefined> {
|
|
const middlewareManifestPath = path.join(
|
|
entryPath,
|
|
outputDirectory,
|
|
'./server/middleware-manifest.json'
|
|
);
|
|
|
|
const hasManifest = await fs
|
|
.access(middlewareManifestPath)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (!hasManifest) {
|
|
return;
|
|
}
|
|
|
|
const manifest = (await fs.readJSON(
|
|
middlewareManifestPath
|
|
)) as MiddlewareManifest;
|
|
|
|
if (manifest.version === 1) {
|
|
return upgradeMiddlewareManifestV1(manifest);
|
|
}
|
|
|
|
if (manifest.version === 2) {
|
|
return upgradeMiddlewareManifestV2(manifest);
|
|
}
|
|
|
|
return manifest;
|
|
}
|
|
|
|
export function upgradeMiddlewareManifestV1(
|
|
v1: MiddlewareManifestV1
|
|
): MiddlewareManifestV3 {
|
|
function updateInfo(v1Info: EdgeFunctionInfoV1): EdgeFunctionInfoV3 {
|
|
const { regexp, ...rest } = v1Info;
|
|
return {
|
|
...rest,
|
|
matchers: [{ regexp }],
|
|
env: {},
|
|
};
|
|
}
|
|
|
|
const middleware = Object.fromEntries(
|
|
Object.entries(v1.middleware).map(([p, info]) => [p, updateInfo(info)])
|
|
);
|
|
const functions = v1.functions
|
|
? Object.fromEntries(
|
|
Object.entries(v1.functions).map(([p, info]) => [p, updateInfo(info)])
|
|
)
|
|
: undefined;
|
|
|
|
return {
|
|
...v1,
|
|
version: 3,
|
|
middleware,
|
|
functions,
|
|
};
|
|
}
|
|
|
|
export function upgradeMiddlewareManifestV2(
|
|
v2: MiddlewareManifestV2
|
|
): MiddlewareManifestV3 {
|
|
function updateInfo(v2Info: EdgeFunctionInfoV2): EdgeFunctionInfoV3 {
|
|
const { ...rest } = v2Info;
|
|
return {
|
|
...rest,
|
|
env: {},
|
|
};
|
|
}
|
|
|
|
const middleware = Object.fromEntries(
|
|
Object.entries(v2.middleware).map(([p, info]) => [p, updateInfo(info)])
|
|
);
|
|
const functions = v2.functions
|
|
? Object.fromEntries(
|
|
Object.entries(v2.functions).map(([p, info]) => [p, updateInfo(info)])
|
|
)
|
|
: undefined;
|
|
|
|
return {
|
|
...v2,
|
|
version: 3,
|
|
middleware,
|
|
functions,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* For an object containing middleware info and a routes manifest this will
|
|
* generate a string with the route that will activate the middleware on
|
|
* Vercel Proxy.
|
|
*
|
|
* @param param0 The middleware info including matchers and page.
|
|
* @param param1 The routes manifest
|
|
* @returns matchers for the middleware route.
|
|
*/
|
|
function getRouteMatchers(
|
|
info: EdgeFunctionInfoV2,
|
|
{ basePath = '', i18n }: RoutesManifest
|
|
): EdgeFunctionMatcher[] {
|
|
function getRegexp(regexp: string) {
|
|
if (info.page === '/') {
|
|
return regexp;
|
|
}
|
|
|
|
const locale = i18n?.locales.length
|
|
? `(?:/(${i18n.locales
|
|
.map(locale => escapeStringRegexp(locale))
|
|
.join('|')}))?`
|
|
: '';
|
|
|
|
return `(?:^${basePath}${locale}${regexp.substring(1)})`;
|
|
}
|
|
|
|
function normalizeHas(has: HasField): HasField {
|
|
return has.map(v =>
|
|
v.type === 'header'
|
|
? {
|
|
...v,
|
|
key: v.key.toLowerCase(),
|
|
}
|
|
: v
|
|
);
|
|
}
|
|
|
|
return info.matchers.map(matcher => {
|
|
const m: EdgeFunctionMatcher = {
|
|
regexp: getRegexp(matcher.regexp),
|
|
originalSource: matcher.originalSource,
|
|
};
|
|
if (matcher.has) {
|
|
m.has = normalizeHas(matcher.has);
|
|
}
|
|
if (matcher.missing) {
|
|
m.missing = normalizeHas(matcher.missing);
|
|
}
|
|
return m;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Makes the sources more human-readable in the source map
|
|
* by removing webpack-specific prefixes
|
|
*/
|
|
function transformSourceMap(
|
|
sourcemap: RawSourceMap | null
|
|
): RawSourceMap | undefined {
|
|
if (!sourcemap) return;
|
|
const sources = sourcemap.sources
|
|
?.map(source => {
|
|
return source.replace(/^webpack:\/\/?_N_E\/(?:\.\/)?/, '');
|
|
})
|
|
// Hide the Next.js entrypoint
|
|
.map(source => {
|
|
return source.startsWith('?') ? '[native code]' : source;
|
|
});
|
|
|
|
return { ...sourcemap, sources };
|
|
}
|
|
|
|
interface LambdaGroupTypeInterface {
|
|
isApiLambda: boolean;
|
|
isPrerenders?: boolean;
|
|
}
|
|
|
|
export function getOperationType({
|
|
group,
|
|
prerenderManifest,
|
|
pageFileName,
|
|
}: {
|
|
group?: LambdaGroupTypeInterface;
|
|
prerenderManifest?: NextPrerenderedRoutes;
|
|
pageFileName?: string;
|
|
}) {
|
|
if (group?.isApiLambda || isApiPage(pageFileName)) {
|
|
return 'API';
|
|
}
|
|
|
|
if (group?.isPrerenders) {
|
|
return 'ISR';
|
|
}
|
|
|
|
if (pageFileName && prerenderManifest) {
|
|
const { blockingFallbackRoutes = {}, fallbackRoutes = {} } =
|
|
prerenderManifest;
|
|
if (
|
|
pageFileName in blockingFallbackRoutes ||
|
|
pageFileName in fallbackRoutes
|
|
) {
|
|
return 'ISR';
|
|
}
|
|
}
|
|
|
|
return 'Page'; // aka SSR
|
|
}
|
|
|
|
export function isApiPage(page: string | undefined) {
|
|
if (!page) {
|
|
return false;
|
|
}
|
|
|
|
return page
|
|
.replace(/\\/g, '/')
|
|
.match(/(serverless|server)\/pages\/api(\/|\.js$)/);
|
|
}
|
|
|
|
export type VariantsManifest = {
|
|
definitions: FlagDefinitions;
|
|
};
|
|
|
|
export async function getVariantsManifest(
|
|
entryPath: string,
|
|
outputDirectory: string
|
|
): Promise<null | VariantsManifest> {
|
|
const pathVariantsManifest = path.join(
|
|
entryPath,
|
|
outputDirectory,
|
|
'variants-manifest.json'
|
|
);
|
|
|
|
const hasVariantsManifest = await fs
|
|
.access(pathVariantsManifest)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (!hasVariantsManifest) return null;
|
|
|
|
const variantsManifest: VariantsManifest = await fs.readJSON(
|
|
pathVariantsManifest
|
|
);
|
|
|
|
return variantsManifest;
|
|
}
|
|
|
|
export async function getServerlessPages(params: {
|
|
pagesDir: string;
|
|
entryPath: string;
|
|
outputDirectory: string;
|
|
appPathRoutesManifest?: Record<string, string>;
|
|
}) {
|
|
const appDir = path.join(params.pagesDir, '../app');
|
|
const [pages, appPaths, middlewareManifest] = await Promise.all([
|
|
glob('**/!(_middleware).js', params.pagesDir),
|
|
params.appPathRoutesManifest
|
|
? Promise.all([
|
|
glob('**/page.js', appDir),
|
|
glob('**/route.js', appDir),
|
|
glob('**/_not-found.js', appDir),
|
|
]).then(items => Object.assign(...items))
|
|
: Promise.resolve({} as Record<string, FileFsRef>),
|
|
getMiddlewareManifest(params.entryPath, params.outputDirectory),
|
|
]);
|
|
|
|
const normalizedAppPaths: typeof appPaths = {};
|
|
|
|
if (params.appPathRoutesManifest) {
|
|
for (const [entry, normalizedEntry] of Object.entries(
|
|
params.appPathRoutesManifest
|
|
)) {
|
|
const normalizedPath = `${path.join(
|
|
'.',
|
|
normalizedEntry === '/' ? '/index' : normalizedEntry
|
|
)}.js`;
|
|
const globPath = `${path.posix.join('.', entry)}.js`;
|
|
|
|
if (appPaths[globPath]) {
|
|
normalizedAppPaths[normalizedPath] = appPaths[globPath];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Edge Functions do not consider as Serverless Functions
|
|
for (const edgeFunctionFile of Object.keys(
|
|
middlewareManifest?.functions ?? {}
|
|
)) {
|
|
let edgePath =
|
|
middlewareManifest?.functions?.[edgeFunctionFile].name ||
|
|
edgeFunctionFile;
|
|
|
|
edgePath = normalizeEdgeFunctionPath(
|
|
edgePath,
|
|
params.appPathRoutesManifest || {}
|
|
);
|
|
edgePath = (edgePath || 'index') + '.js';
|
|
delete normalizedAppPaths[edgePath];
|
|
delete pages[edgePath];
|
|
}
|
|
|
|
return { pages, appPaths: normalizedAppPaths };
|
|
}
|
|
|
|
// to avoid any conflict with route matching/resolving, we prefix all prefetches (ie, __index.prefetch.rsc)
|
|
// this is to ensure that prefetches are never matched for things like a greedy match on `index.{ext}`
|
|
export function normalizePrefetches(prefetches: Record<string, FileFsRef>) {
|
|
const updatedPrefetches: Record<string, FileFsRef> = {};
|
|
|
|
for (const key in prefetches) {
|
|
if (key === 'index.prefetch.rsc') {
|
|
const newKey = key.replace(/([^/]+\.prefetch\.rsc)$/, '__$1');
|
|
updatedPrefetches[newKey] = prefetches[key];
|
|
} else {
|
|
updatedPrefetches[key] = prefetches[key];
|
|
}
|
|
}
|
|
|
|
return updatedPrefetches;
|
|
}
|