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 & { version: 4; dynamicRoutes: (RoutesManifestRoute | { page: string; isMiddleware: true })[]; }; export type RoutesManifest = RoutesManifestV4 | RoutesManifestOld; export async function getRoutesManifest( entryPath: string, outputDirectory: string, nextVersion?: string ): Promise { 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; canUsePreviewMode?: boolean; bypassToken?: string; isServerMode?: boolean; dynamicMiddlewareRouteMap?: ReadonlyMap; hasActionOutputSupport: boolean; isAppPPREnabled: boolean; }): Promise { 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('(?:/)?$')), '(?(?:\\.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[]) | 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 ): 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 ? '' : ':' }${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 { 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, reasons: NodeFileTraceReasons, ignoreFn?: (file: string, parent?: string) => boolean ): ReadonlyMap> { // 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>(); function propagateToParents( parents: Set, file: string, seen = new Set() ) { 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 }, 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 { 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; }; 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; 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 { 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 { 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 { 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 { 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; 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; 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 { 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; }) { 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; prerenderRoutes: ReadonlySet; experimentalPPRRoutes: ReadonlySet | undefined; pageTraces: { [page: string]: { [key: string]: FileFsRef; }; }; compressedPages: { [page: string]: PseudoFile; }; tracedPseudoLayer: PseudoLayer; initialPseudoLayer: PseudoLayerResult; initialPseudoLayerUncompressed: number; internalPages: ReadonlyArray; pageExtensions?: ReadonlyArray; inversedAppPathManifest?: Record; experimentalAllowBundling?: boolean; }) { const groups: Array = []; 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 ) { 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 = []; 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, 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; prerenderManifest: NextPrerenderedRoutes; isSharedLambdas: boolean; isServerMode: boolean; canUsePreviewMode: boolean; lambdas: { [key: string]: Lambda }; experimentalStreamingLambdaPaths: ReadonlyMap | 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 | 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 extends Promise ? 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> = {}; 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 = {}; const staticDirectoryFiles: Record = {}; const publicDirectoryFiles: Record = {}; 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 ) { 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; } 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 ) { 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; }): Promise<{ staticRoutes: Route[]; dynamicRouteMap: ReadonlyMap; edgeFunctions: Record; }> { 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; edgeFunctions: Record; } = { 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 { 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 { 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 { 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; }) { 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), 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) { const updatedPrefetches: Record = {}; 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; }