rsnext/packages/next/build/index.ts
JJ Kasper a6c0d76ebb
Run server client/server builds serially (#23371)
Previously we special cased serverless builds and ran the client/server builds serially to allow the server build to load manifests produced in the client. To help with memory usage and for consistency this updates server mode to build in the same way.  

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.

## Documentation / Examples

- [ ] Make sure the linting passes
2021-03-24 20:50:49 +00:00

1543 lines
50 KiB
TypeScript

import { loadEnvConfig } from '@next/env'
import chalk from 'chalk'
import crypto from 'crypto'
import { promises, writeFileSync } from 'fs'
import { Worker } from 'jest-worker'
import devalue from 'next/dist/compiled/devalue'
import escapeStringRegexp from 'next/dist/compiled/escape-string-regexp'
import findUp from 'next/dist/compiled/find-up'
import { nanoid } from 'next/dist/compiled/nanoid/index.cjs'
import { pathToRegexp } from 'next/dist/compiled/path-to-regexp'
import path from 'path'
import formatWebpackMessages from '../client/dev/error-overlay/format-webpack-messages'
import {
STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR,
PUBLIC_DIR_MIDDLEWARE_CONFLICT,
} from '../lib/constants'
import { fileExists } from '../lib/file-exists'
import { findPagesDir } from '../lib/find-pages-dir'
import loadCustomRoutes, {
getRedirectStatus,
normalizeRouteRegex,
Redirect,
RouteType,
} from '../lib/load-custom-routes'
import { nonNullable } from '../lib/non-nullable'
import { recursiveDelete } from '../lib/recursive-delete'
import { verifyTypeScriptSetup } from '../lib/verifyTypeScriptSetup'
import {
BUILD_ID_FILE,
BUILD_MANIFEST,
CLIENT_STATIC_FILES_PATH,
EXPORT_DETAIL,
EXPORT_MARKER,
FONT_MANIFEST,
IMAGES_MANIFEST,
PAGES_MANIFEST,
PHASE_PRODUCTION_BUILD,
PRERENDER_MANIFEST,
REACT_LOADABLE_MANIFEST,
ROUTES_MANIFEST,
SERVERLESS_DIRECTORY,
SERVER_DIRECTORY,
SERVER_FILES_MANIFEST,
STATIC_STATUS_PAGES,
} from '../next-server/lib/constants'
import {
getRouteRegex,
getSortedRoutes,
isDynamicRoute,
} from '../next-server/lib/router/utils'
import { __ApiPreviewProps } from '../next-server/server/api-utils'
import loadConfig, {
isTargetLikeServerless,
} from '../next-server/server/config'
import { BuildManifest } from '../next-server/server/get-page-files'
import '../next-server/server/node-polyfill-fetch'
import { normalizePagePath } from '../next-server/server/normalize-page-path'
import { getPagePath } from '../next-server/server/require'
import * as ciEnvironment from '../telemetry/ci-info'
import {
eventBuildCompleted,
eventBuildOptimize,
eventCliSession,
eventNextPlugins,
} from '../telemetry/events'
import { Telemetry } from '../telemetry/storage'
import { CompilerResult, runCompiler } from './compiler'
import { createEntrypoints, createPagesMapping } from './entries'
import { generateBuildId } from './generate-build-id'
import { isWriteable } from './is-writeable'
import * as Log from './output/log'
import createSpinner from './spinner'
import { trace, setGlobal } from '../telemetry/trace'
import {
collectPages,
detectConflictingPaths,
getJsPageSizeInKb,
getNamedExports,
hasCustomGetInitialProps,
isPageStatic,
PageInfo,
printCustomRoutes,
printTreeView,
getCssFilePaths,
} from './utils'
import getBaseWebpackConfig from './webpack-config'
import { PagesManifest } from './webpack/plugins/pages-manifest-plugin'
import { writeBuildId } from './write-build-id'
import { normalizeLocalePath } from '../next-server/lib/i18n/normalize-locale-path'
const staticCheckWorker = require.resolve('./utils')
export type SsgRoute = {
initialRevalidateSeconds: number | false
srcRoute: string | null
dataRoute: string
}
export type DynamicSsgRoute = {
routeRegex: string
fallback: string | null | false
dataRoute: string
dataRouteRegex: string
}
export type PrerenderManifest = {
version: 3
routes: { [route: string]: SsgRoute }
dynamicRoutes: { [route: string]: DynamicSsgRoute }
notFoundRoutes: string[]
preview: __ApiPreviewProps
}
export default async function build(
dir: string,
conf = null,
reactProductionProfiling = false,
debugOutput = false
): Promise<void> {
const nextBuildSpan = trace('next-build')
return nextBuildSpan.traceAsyncFn(async () => {
// attempt to load global env values so they are available in next.config.js
const { loadedEnvFiles } = nextBuildSpan
.traceChild('load-dotenv')
.traceFn(() => loadEnvConfig(dir, false, Log))
const config = await nextBuildSpan
.traceChild('load-next-config')
.traceAsyncFn(() => loadConfig(PHASE_PRODUCTION_BUILD, dir, conf))
const { target } = config
const buildId = await nextBuildSpan
.traceChild('generate-buildid')
.traceAsyncFn(() => generateBuildId(config.generateBuildId, nanoid))
const distDir = path.join(dir, config.distDir)
const { headers, rewrites, redirects } = await nextBuildSpan
.traceChild('load-custom-routes')
.traceAsyncFn(() => loadCustomRoutes(config))
if (ciEnvironment.isCI && !ciEnvironment.hasNextSupport) {
const cacheDir = path.join(distDir, 'cache')
const hasCache = await fileExists(cacheDir)
if (!hasCache) {
// Intentionally not piping to stderr in case people fail in CI when
// stderr is detected.
console.log(
`${Log.prefixes.warn} No build cache found. Please configure build caching for faster rebuilds. Read more: https://err.sh/next.js/no-cache`
)
}
}
const typeCheckingSpinner = createSpinner({
prefixText: `${Log.prefixes.info} Checking validity of types`,
})
const telemetry = new Telemetry({ distDir })
setGlobal('telemetry', telemetry)
const publicDir = path.join(dir, 'public')
const pagesDir = findPagesDir(dir)
const hasPublicDir = await fileExists(publicDir)
telemetry.record(
eventCliSession(PHASE_PRODUCTION_BUILD, dir, {
cliCommand: 'build',
isSrcDir: path.relative(dir, pagesDir!).startsWith('src'),
hasNowJson: !!(await findUp('now.json', { cwd: dir })),
isCustomServer: null,
})
)
eventNextPlugins(path.resolve(dir)).then((events) =>
telemetry.record(events)
)
if (config.eslint?.build) {
await nextBuildSpan
.traceChild('verify-and-lint')
.traceAsyncFn(async () => {
const lintWorkers = new Worker(
require.resolve('../lib/verifyAndLint'),
{
numWorkers: config.experimental.cpus,
enableWorkerThreads: config.experimental.workerThreads,
}
) as Worker & {
verifyAndLint: typeof import('../lib/verifyAndLint').verifyAndLint
}
lintWorkers.getStdout().pipe(process.stdout)
lintWorkers.getStderr().pipe(process.stderr)
const lintResults = await lintWorkers.verifyAndLint(
dir,
pagesDir,
null
)
if (lintResults.hasErrors) {
console.error(chalk.red('Failed to compile.'))
console.error(lintResults.results)
process.exit(1)
} else if (lintResults.hasMessages) {
console.log(lintResults.results)
}
lintWorkers.end()
})
}
const ignoreTypeScriptErrors = Boolean(config.typescript?.ignoreBuildErrors)
await nextBuildSpan
.traceChild('verify-typescript-setup')
.traceAsyncFn(() =>
verifyTypeScriptSetup(dir, pagesDir, !ignoreTypeScriptErrors)
)
if (typeCheckingSpinner) {
typeCheckingSpinner.stopAndPersist()
}
const buildSpinner = createSpinner({
prefixText: `${Log.prefixes.info} Creating an optimized production build`,
})
const isLikeServerless = isTargetLikeServerless(target)
const pagePaths: string[] = await nextBuildSpan
.traceChild('collect-pages')
.traceAsyncFn(() => collectPages(pagesDir, config.pageExtensions))
// needed for static exporting since we want to replace with HTML
// files
const allStaticPages = new Set<string>()
let allPageInfos = new Map<string, PageInfo>()
const previewProps: __ApiPreviewProps = {
previewModeId: crypto.randomBytes(16).toString('hex'),
previewModeSigningKey: crypto.randomBytes(32).toString('hex'),
previewModeEncryptionKey: crypto.randomBytes(32).toString('hex'),
}
const mappedPages = nextBuildSpan
.traceChild('create-pages-mapping')
.traceFn(() => createPagesMapping(pagePaths, config.pageExtensions))
const entrypoints = nextBuildSpan
.traceChild('create-entrypoints')
.traceFn(() =>
createEntrypoints(
mappedPages,
target,
buildId,
previewProps,
config,
loadedEnvFiles
)
)
const pageKeys = Object.keys(mappedPages)
const conflictingPublicFiles: string[] = []
const hasCustomErrorPage = mappedPages['/_error'].startsWith(
'private-next-pages'
)
const hasPages404 = Boolean(
mappedPages['/404'] &&
mappedPages['/404'].startsWith('private-next-pages')
)
if (hasPublicDir) {
const hasPublicUnderScoreNextDir = await fileExists(
path.join(publicDir, '_next')
)
if (hasPublicUnderScoreNextDir) {
throw new Error(PUBLIC_DIR_MIDDLEWARE_CONFLICT)
}
}
await nextBuildSpan
.traceChild('public-dir-conflict-check')
.traceAsyncFn(async () => {
// Check if pages conflict with files in `public`
// Only a page of public file can be served, not both.
for (const page in mappedPages) {
const hasPublicPageFile = await fileExists(
path.join(publicDir, page === '/' ? '/index' : page),
'file'
)
if (hasPublicPageFile) {
conflictingPublicFiles.push(page)
}
}
const numConflicting = conflictingPublicFiles.length
if (numConflicting) {
throw new Error(
`Conflicting public and page file${
numConflicting === 1 ? ' was' : 's were'
} found. https://err.sh/vercel/next.js/conflicting-public-file-page\n${conflictingPublicFiles.join(
'\n'
)}`
)
}
})
const nestedReservedPages = pageKeys.filter((page) => {
return (
page.match(/\/(_app|_document|_error)$/) && path.dirname(page) !== '/'
)
})
if (nestedReservedPages.length) {
Log.warn(
`The following reserved Next.js pages were detected not directly under the pages directory:\n` +
nestedReservedPages.join('\n') +
`\nSee more info here: https://err.sh/next.js/nested-reserved-page\n`
)
}
const buildCustomRoute = (
r: {
source: string
locale?: false
basePath?: false
statusCode?: number
destination?: string
},
type: RouteType
) => {
const keys: any[] = []
const routeRegex = pathToRegexp(r.source, keys, {
strict: true,
sensitive: false,
delimiter: '/', // default is `/#?`, but Next does not pass query info
})
return {
...r,
...(type === 'redirect'
? {
statusCode: getRedirectStatus(r as Redirect),
permanent: undefined,
}
: {}),
regex: normalizeRouteRegex(routeRegex.source),
}
}
const routesManifestPath = path.join(distDir, ROUTES_MANIFEST)
const routesManifest: {
version: number
pages404: boolean
basePath: string
redirects: Array<ReturnType<typeof buildCustomRoute>>
rewrites: Array<ReturnType<typeof buildCustomRoute>>
headers: Array<ReturnType<typeof buildCustomRoute>>
dynamicRoutes: Array<{
page: string
regex: string
namedRegex?: string
routeKeys?: { [key: string]: string }
}>
dataRoutes: Array<{
page: string
routeKeys?: { [key: string]: string }
dataRouteRegex: string
namedDataRouteRegex?: string
}>
i18n?: {
domains?: Array<{
http?: true
domain: string
locales?: string[]
defaultLocale: string
}>
locales: string[]
defaultLocale: string
localeDetection?: false
}
} = nextBuildSpan.traceChild('generate-routes-manifest').traceFn(() => ({
version: 3,
pages404: true,
basePath: config.basePath,
redirects: redirects.map((r: any) => buildCustomRoute(r, 'redirect')),
rewrites: rewrites.map((r: any) => buildCustomRoute(r, 'rewrite')),
headers: headers.map((r: any) => buildCustomRoute(r, 'header')),
dynamicRoutes: getSortedRoutes(pageKeys)
.filter(isDynamicRoute)
.map((page) => {
const routeRegex = getRouteRegex(page)
return {
page,
regex: normalizeRouteRegex(routeRegex.re.source),
routeKeys: routeRegex.routeKeys,
namedRegex: routeRegex.namedRegex,
}
}),
dataRoutes: [],
i18n: config.i18n || undefined,
}))
const distDirCreated = await nextBuildSpan
.traceChild('create-dist-dir')
.traceAsyncFn(async () => {
try {
await promises.mkdir(distDir, { recursive: true })
return true
} catch (err) {
if (err.code === 'EPERM') {
return false
}
throw err
}
})
if (!distDirCreated || !(await isWriteable(distDir))) {
throw new Error(
'> Build directory is not writeable. https://err.sh/vercel/next.js/build-dir-not-writeable'
)
}
// We need to write the manifest with rewrites before build
// so serverless can import the manifest
await nextBuildSpan
.traceChild('write-routes-manifest')
.traceAsyncFn(() =>
promises.writeFile(
routesManifestPath,
JSON.stringify(routesManifest),
'utf8'
)
)
const manifestPath = path.join(
distDir,
isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
PAGES_MANIFEST
)
const requiredServerFiles = nextBuildSpan
.traceChild('generate-required-server-files')
.traceFn(() => ({
version: 1,
config: {
...config,
compress: false,
configFile: undefined,
},
appDir: dir,
files: [
ROUTES_MANIFEST,
path.relative(distDir, manifestPath),
BUILD_MANIFEST,
PRERENDER_MANIFEST,
REACT_LOADABLE_MANIFEST,
config.experimental.optimizeFonts
? path.join(
isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
FONT_MANIFEST
)
: null,
BUILD_ID_FILE,
]
.filter(nonNullable)
.map((file) => path.join(config.distDir, file)),
ignore: [] as string[],
}))
const configs = await nextBuildSpan
.traceChild('generate-webpack-config')
.traceAsyncFn(() =>
Promise.all([
getBaseWebpackConfig(dir, {
buildId,
reactProductionProfiling,
isServer: false,
config,
target,
pagesDir,
entrypoints: entrypoints.client,
rewrites,
}),
getBaseWebpackConfig(dir, {
buildId,
reactProductionProfiling,
isServer: true,
config,
target,
pagesDir,
entrypoints: entrypoints.server,
rewrites,
}),
])
)
const clientConfig = configs[0]
if (
clientConfig.optimization &&
(clientConfig.optimization.minimize !== true ||
(clientConfig.optimization.minimizer &&
clientConfig.optimization.minimizer.length === 0))
) {
Log.warn(
`Production code optimization has been disabled in your project. Read more: https://err.sh/vercel/next.js/minification-disabled`
)
}
const webpackBuildStart = process.hrtime()
let result: CompilerResult = { warnings: [], errors: [] }
// We run client and server compilation separately when configured for
// memory constraint and for serverless to be able to load manifests
// produced in the client build
if (isLikeServerless || config.experimental.serialWebpackBuild) {
await nextBuildSpan
.traceChild('run-webpack-compiler')
.traceAsyncFn(async () => {
const clientResult = await runCompiler(clientConfig)
// Fail build if clientResult contains errors
if (clientResult.errors.length > 0) {
result = {
warnings: [...clientResult.warnings],
errors: [...clientResult.errors],
}
} else {
const serverResult = await runCompiler(configs[1])
result = {
warnings: [...clientResult.warnings, ...serverResult.warnings],
errors: [...clientResult.errors, ...serverResult.errors],
}
}
})
} else {
result = await nextBuildSpan
.traceChild('run-webpack-compiler')
.traceAsyncFn(() => runCompiler(configs))
}
const webpackBuildEnd = process.hrtime(webpackBuildStart)
if (buildSpinner) {
buildSpinner.stopAndPersist()
}
result = nextBuildSpan
.traceChild('format-webpack-messages')
.traceFn(() => formatWebpackMessages(result))
if (result.errors.length > 0) {
// Only keep the first error. Others are often indicative
// of the same problem, but confuse the reader with noise.
if (result.errors.length > 1) {
result.errors.length = 1
}
const error = result.errors.join('\n\n')
console.error(chalk.red('Failed to compile.\n'))
if (
error.indexOf('private-next-pages') > -1 &&
error.indexOf('does not contain a default export') > -1
) {
const page_name_regex = /'private-next-pages\/(?<page_name>[^']*)'/
const parsed = page_name_regex.exec(error)
const page_name = parsed && parsed.groups && parsed.groups.page_name
throw new Error(
`webpack build failed: found page without a React Component as default export in pages/${page_name}\n\nSee https://err.sh/vercel/next.js/page-without-valid-component for more info.`
)
}
console.error(error)
console.error()
if (
error.indexOf('private-next-pages') > -1 ||
error.indexOf('__next_polyfill__') > -1
) {
throw new Error(
'> webpack config.resolve.alias was incorrectly overridden. https://err.sh/vercel/next.js/invalid-resolve-alias'
)
}
throw new Error('> Build failed because of webpack errors')
} else {
telemetry.record(
eventBuildCompleted(pagePaths, {
durationInSeconds: webpackBuildEnd[0],
})
)
if (result.warnings.length > 0) {
Log.warn('Compiled with warnings\n')
console.warn(result.warnings.join('\n\n'))
console.warn()
} else {
Log.info('Compiled successfully')
}
}
const postCompileSpinner = createSpinner({
prefixText: `${Log.prefixes.info} Collecting page data`,
})
const buildManifestPath = path.join(distDir, BUILD_MANIFEST)
const ssgPages = new Set<string>()
const ssgStaticFallbackPages = new Set<string>()
const ssgBlockingFallbackPages = new Set<string>()
const staticPages = new Set<string>()
const invalidPages = new Set<string>()
const hybridAmpPages = new Set<string>()
const serverPropsPages = new Set<string>()
const additionalSsgPaths = new Map<string, Array<string>>()
const additionalSsgPathsEncoded = new Map<string, Array<string>>()
const pageInfos = new Map<string, PageInfo>()
const pagesManifest = JSON.parse(
await promises.readFile(manifestPath, 'utf8')
) as PagesManifest
const buildManifest = JSON.parse(
await promises.readFile(buildManifestPath, 'utf8')
) as BuildManifest
let customAppGetInitialProps: boolean | undefined
let namedExports: Array<string> | undefined
let isNextImageImported: boolean | undefined
const analysisBegin = process.hrtime()
let hasSsrAmpPages = false
const staticCheckSpan = nextBuildSpan.traceChild('static-check')
const { hasNonStaticErrorPage } = await staticCheckSpan.traceAsyncFn(
async () => {
process.env.NEXT_PHASE = PHASE_PRODUCTION_BUILD
const staticCheckWorkers = new Worker(staticCheckWorker, {
numWorkers: config.experimental.cpus,
enableWorkerThreads: config.experimental.workerThreads,
}) as Worker & { isPageStatic: typeof isPageStatic }
staticCheckWorkers.getStdout().pipe(process.stdout)
staticCheckWorkers.getStderr().pipe(process.stderr)
const runtimeEnvConfig = {
publicRuntimeConfig: config.publicRuntimeConfig,
serverRuntimeConfig: config.serverRuntimeConfig,
}
const nonStaticErrorPageSpan = staticCheckSpan.traceChild(
'check-static-error-page'
)
const nonStaticErrorPage = await nonStaticErrorPageSpan.traceAsyncFn(
async () =>
hasCustomErrorPage &&
(await hasCustomGetInitialProps(
'/_error',
distDir,
isLikeServerless,
runtimeEnvConfig,
false
))
)
// we don't output _app in serverless mode so use _app export
// from _error instead
const appPageToCheck = isLikeServerless ? '/_error' : '/_app'
customAppGetInitialProps = await hasCustomGetInitialProps(
appPageToCheck,
distDir,
isLikeServerless,
runtimeEnvConfig,
true
)
namedExports = await getNamedExports(
appPageToCheck,
distDir,
isLikeServerless,
runtimeEnvConfig
)
if (customAppGetInitialProps) {
console.warn(
chalk.bold.yellow(`Warning: `) +
chalk.yellow(
`You have opted-out of Automatic Static Optimization due to \`getInitialProps\` in \`pages/_app\`. This does not opt-out pages with \`getStaticProps\``
)
)
console.warn(
'Read more: https://err.sh/next.js/opt-out-auto-static-optimization\n'
)
}
await Promise.all(
pageKeys.map(async (page) => {
const checkPageSpan = staticCheckSpan.traceChild('check-page', {
page,
})
return checkPageSpan.traceAsyncFn(async () => {
const actualPage = normalizePagePath(page)
const [selfSize, allSize] = await getJsPageSizeInKb(
actualPage,
distDir,
buildManifest
)
let isSsg = false
let isStatic = false
let isHybridAmp = false
let ssgPageRoutes: string[] | null = null
const nonReservedPage = !page.match(
/^\/(_app|_error|_document|api(\/|$))/
)
if (nonReservedPage) {
try {
let isPageStaticSpan = checkPageSpan.traceChild(
'is-page-static'
)
let workerResult = await isPageStaticSpan.traceAsyncFn(() => {
return staticCheckWorkers.isPageStatic(
page,
distDir,
isLikeServerless,
runtimeEnvConfig,
config.i18n?.locales,
config.i18n?.defaultLocale,
isPageStaticSpan.id
)
})
if (
workerResult.isStatic === false &&
(workerResult.isHybridAmp || workerResult.isAmpOnly)
) {
hasSsrAmpPages = true
}
if (workerResult.isHybridAmp) {
isHybridAmp = true
hybridAmpPages.add(page)
}
if (workerResult.isNextImageImported) {
isNextImageImported = true
}
if (workerResult.hasStaticProps) {
ssgPages.add(page)
isSsg = true
if (
workerResult.prerenderRoutes &&
workerResult.encodedPrerenderRoutes
) {
additionalSsgPaths.set(page, workerResult.prerenderRoutes)
additionalSsgPathsEncoded.set(
page,
workerResult.encodedPrerenderRoutes
)
ssgPageRoutes = workerResult.prerenderRoutes
}
if (workerResult.prerenderFallback === 'blocking') {
ssgBlockingFallbackPages.add(page)
} else if (workerResult.prerenderFallback === true) {
ssgStaticFallbackPages.add(page)
}
} else if (workerResult.hasServerProps) {
serverPropsPages.add(page)
} else if (
workerResult.isStatic &&
customAppGetInitialProps === false
) {
staticPages.add(page)
isStatic = true
}
if (hasPages404 && page === '/404') {
if (
!workerResult.isStatic &&
!workerResult.hasStaticProps
) {
throw new Error(
`\`pages/404\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
)
}
// we need to ensure the 404 lambda is present since we use
// it when _app has getInitialProps
if (
customAppGetInitialProps &&
!workerResult.hasStaticProps
) {
staticPages.delete(page)
}
}
if (
STATIC_STATUS_PAGES.includes(page) &&
!workerResult.isStatic &&
!workerResult.hasStaticProps
) {
throw new Error(
`\`pages${page}\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}`
)
}
} catch (err) {
if (err.message !== 'INVALID_DEFAULT_EXPORT') throw err
invalidPages.add(page)
}
}
pageInfos.set(page, {
size: selfSize,
totalSize: allSize,
static: isStatic,
isSsg,
isHybridAmp,
ssgPageRoutes,
initialRevalidateSeconds: false,
})
})
})
)
staticCheckWorkers.end()
return { hasNonStaticErrorPage: nonStaticErrorPage }
}
)
if (!hasSsrAmpPages) {
requiredServerFiles.ignore.push(
path.relative(
dir,
path.join(
path.dirname(
require.resolve(
'next/dist/compiled/@ampproject/toolbox-optimizer'
)
),
'**/*'
)
)
)
}
if (serverPropsPages.size > 0 || ssgPages.size > 0) {
// We update the routes manifest after the build with the
// data routes since we can't determine these until after build
routesManifest.dataRoutes = getSortedRoutes([
...serverPropsPages,
...ssgPages,
]).map((page) => {
const pagePath = normalizePagePath(page)
const dataRoute = path.posix.join(
'/_next/data',
buildId,
`${pagePath}.json`
)
let dataRouteRegex: string
let namedDataRouteRegex: string | undefined
let routeKeys: { [named: string]: string } | undefined
if (isDynamicRoute(page)) {
const routeRegex = getRouteRegex(dataRoute.replace(/\.json$/, ''))
dataRouteRegex = normalizeRouteRegex(
routeRegex.re.source.replace(/\(\?:\\\/\)\?\$$/, '\\.json$')
)
namedDataRouteRegex = routeRegex.namedRegex!.replace(
/\(\?:\/\)\?\$$/,
'\\.json$'
)
routeKeys = routeRegex.routeKeys
} else {
dataRouteRegex = normalizeRouteRegex(
new RegExp(
`^${path.posix.join(
'/_next/data',
escapeStringRegexp(buildId),
`${pagePath}.json`
)}$`
).source
)
}
return {
page,
routeKeys,
dataRouteRegex,
namedDataRouteRegex,
}
})
await promises.writeFile(
routesManifestPath,
JSON.stringify(routesManifest),
'utf8'
)
}
// Since custom _app.js can wrap the 404 page we have to opt-out of static optimization if it has getInitialProps
// Only export the static 404 when there is no /_error present
const useStatic404 =
!customAppGetInitialProps && (!hasNonStaticErrorPage || hasPages404)
if (invalidPages.size > 0) {
throw new Error(
`Build optimization failed: found page${
invalidPages.size === 1 ? '' : 's'
} without a React Component as default export in \n${[...invalidPages]
.map((pg) => `pages${pg}`)
.join(
'\n'
)}\n\nSee https://err.sh/vercel/next.js/page-without-valid-component for more info.\n`
)
}
await writeBuildId(distDir, buildId)
if (config.experimental.optimizeCss) {
const cssFilePaths = getCssFilePaths(buildManifest)
requiredServerFiles.files.push(
...cssFilePaths.map((filePath) => path.join(config.distDir, filePath))
)
}
await promises.writeFile(
path.join(distDir, SERVER_FILES_MANIFEST),
JSON.stringify(requiredServerFiles),
'utf8'
)
const finalPrerenderRoutes: { [route: string]: SsgRoute } = {}
const tbdPrerenderRoutes: string[] = []
let ssgNotFoundPaths: string[] = []
if (postCompileSpinner) postCompileSpinner.stopAndPersist()
const { i18n } = config
const usedStaticStatusPages = STATIC_STATUS_PAGES.filter(
(page) =>
mappedPages[page] && mappedPages[page].startsWith('private-next-pages')
)
usedStaticStatusPages.forEach((page) => {
if (!ssgPages.has(page)) {
staticPages.add(page)
}
})
const hasPages500 = usedStaticStatusPages.includes('/500')
const useDefaultStatic500 = !hasPages500 && !hasNonStaticErrorPage
const combinedPages = [...staticPages, ...ssgPages]
if (combinedPages.length > 0 || useStatic404 || useDefaultStatic500) {
const staticGenerationSpan = nextBuildSpan.traceChild('static-generation')
await staticGenerationSpan.traceAsyncFn(async () => {
detectConflictingPaths(
[
...combinedPages,
...pageKeys.filter((page) => !combinedPages.includes(page)),
],
ssgPages,
additionalSsgPaths
)
const exportApp = require('../export').default
const exportOptions = {
silent: false,
buildExport: true,
threads: config.experimental.cpus,
pages: combinedPages,
outdir: path.join(distDir, 'export'),
statusMessage: 'Generating static pages',
}
const exportConfig: any = {
...config,
initialPageRevalidationMap: {},
ssgNotFoundPaths: [] as string[],
// Default map will be the collection of automatic statically exported
// pages and incremental pages.
// n.b. we cannot handle this above in combinedPages because the dynamic
// page must be in the `pages` array, but not in the mapping.
exportPathMap: (defaultMap: any) => {
// Dynamically routed pages should be prerendered to be used as
// a client-side skeleton (fallback) while data is being fetched.
// This ensures the end-user never sees a 500 or slow response from the
// server.
//
// Note: prerendering disables automatic static optimization.
ssgPages.forEach((page) => {
if (isDynamicRoute(page)) {
tbdPrerenderRoutes.push(page)
if (ssgStaticFallbackPages.has(page)) {
// Override the rendering for the dynamic page to be treated as a
// fallback render.
if (i18n) {
defaultMap[`/${i18n.defaultLocale}${page}`] = {
page,
query: { __nextFallback: true },
}
} else {
defaultMap[page] = { page, query: { __nextFallback: true } }
}
} else {
// Remove dynamically routed pages from the default path map when
// fallback behavior is disabled.
delete defaultMap[page]
}
}
})
// Append the "well-known" routes we should prerender for, e.g. blog
// post slugs.
additionalSsgPaths.forEach((routes, page) => {
const encodedRoutes = additionalSsgPathsEncoded.get(page)
routes.forEach((route, routeIdx) => {
defaultMap[route] = {
page,
query: { __nextSsgPath: encodedRoutes?.[routeIdx] },
}
})
})
if (useStatic404) {
defaultMap['/404'] = {
page: hasPages404 ? '/404' : '/_error',
}
}
if (useDefaultStatic500) {
defaultMap['/500'] = {
page: '/_error',
}
}
if (i18n) {
for (const page of [
...staticPages,
...ssgPages,
...(useStatic404 ? ['/404'] : []),
...(useDefaultStatic500 ? ['/500'] : []),
]) {
const isSsg = ssgPages.has(page)
const isDynamic = isDynamicRoute(page)
const isFallback = isSsg && ssgStaticFallbackPages.has(page)
for (const locale of i18n.locales) {
// skip fallback generation for SSG pages without fallback mode
if (isSsg && isDynamic && !isFallback) continue
const outputPath = `/${locale}${page === '/' ? '' : page}`
defaultMap[outputPath] = {
page: defaultMap[page]?.page || page,
query: { __nextLocale: locale },
}
if (isFallback) {
defaultMap[outputPath].query.__nextFallback = true
}
}
if (isSsg) {
// remove non-locale prefixed variant from defaultMap
delete defaultMap[page]
}
}
}
return defaultMap
},
}
await exportApp(dir, exportOptions, exportConfig)
const postBuildSpinner = createSpinner({
prefixText: `${Log.prefixes.info} Finalizing page optimization`,
})
ssgNotFoundPaths = exportConfig.ssgNotFoundPaths
// remove server bundles that were exported
for (const page of staticPages) {
const serverBundle = getPagePath(page, distDir, isLikeServerless)
await promises.unlink(serverBundle)
}
const serverOutputDir = path.join(
distDir,
isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
)
const moveExportedPage = async (
originPage: string,
page: string,
file: string,
isSsg: boolean,
ext: 'html' | 'json',
additionalSsgFile = false
) => {
return staticGenerationSpan
.traceChild('move-exported-page')
.traceAsyncFn(async () => {
file = `${file}.${ext}`
const orig = path.join(exportOptions.outdir, file)
const pagePath = getPagePath(
originPage,
distDir,
isLikeServerless
)
const relativeDest = path
.relative(
serverOutputDir,
path.join(
path.join(
pagePath,
// strip leading / and then recurse number of nested dirs
// to place from base folder
originPage
.substr(1)
.split('/')
.map(() => '..')
.join('/')
),
file
)
)
.replace(/\\/g, '/')
const dest = path.join(
distDir,
isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
relativeDest
)
if (
!isSsg &&
!(
// don't add static status page to manifest if it's
// the default generated version e.g. no pages/500
(
STATIC_STATUS_PAGES.includes(page) &&
!usedStaticStatusPages.includes(page)
)
)
) {
pagesManifest[page] = relativeDest
}
const isNotFound = ssgNotFoundPaths.includes(page)
// for SSG files with i18n the non-prerendered variants are
// output with the locale prefixed so don't attempt moving
// without the prefix
if ((!i18n || additionalSsgFile) && !isNotFound) {
await promises.mkdir(path.dirname(dest), { recursive: true })
await promises.rename(orig, dest)
} else if (i18n && !isSsg) {
// this will be updated with the locale prefixed variant
// since all files are output with the locale prefix
delete pagesManifest[page]
}
if (i18n) {
if (additionalSsgFile) return
for (const locale of i18n.locales) {
const curPath = `/${locale}${page === '/' ? '' : page}`
const localeExt = page === '/' ? path.extname(file) : ''
const relativeDestNoPages = relativeDest.substr(
'pages/'.length
)
if (isSsg && ssgNotFoundPaths.includes(curPath)) {
continue
}
const updatedRelativeDest = path
.join(
'pages',
locale + localeExt,
// if it's the top-most index page we want it to be locale.EXT
// instead of locale/index.html
page === '/' ? '' : relativeDestNoPages
)
.replace(/\\/g, '/')
const updatedOrig = path.join(
exportOptions.outdir,
locale + localeExt,
page === '/' ? '' : file
)
const updatedDest = path.join(
distDir,
isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
updatedRelativeDest
)
if (!isSsg) {
pagesManifest[curPath] = updatedRelativeDest
}
await promises.mkdir(path.dirname(updatedDest), {
recursive: true,
})
await promises.rename(updatedOrig, updatedDest)
}
}
})
}
// Only move /404 to /404 when there is no custom 404 as in that case we don't know about the 404 page
if (!hasPages404 && useStatic404) {
await moveExportedPage('/_error', '/404', '/404', false, 'html')
}
if (useDefaultStatic500) {
await moveExportedPage('/_error', '/500', '/500', false, 'html')
}
for (const page of combinedPages) {
const isSsg = ssgPages.has(page)
const isStaticSsgFallback = ssgStaticFallbackPages.has(page)
const isDynamic = isDynamicRoute(page)
const hasAmp = hybridAmpPages.has(page)
const file = normalizePagePath(page)
// The dynamic version of SSG pages are only prerendered if the
// fallback is enabled. Below, we handle the specific prerenders
// of these.
const hasHtmlOutput = !(isSsg && isDynamic && !isStaticSsgFallback)
if (hasHtmlOutput) {
await moveExportedPage(page, page, file, isSsg, 'html')
}
if (hasAmp && (!isSsg || (isSsg && !isDynamic))) {
const ampPage = `${file}.amp`
await moveExportedPage(page, ampPage, ampPage, isSsg, 'html')
if (isSsg) {
await moveExportedPage(page, ampPage, ampPage, isSsg, 'json')
}
}
if (isSsg) {
// For a non-dynamic SSG page, we must copy its data file
// from export, we already moved the HTML file above
if (!isDynamic) {
await moveExportedPage(page, page, file, isSsg, 'json')
if (i18n) {
// TODO: do we want to show all locale variants in build output
for (const locale of i18n.locales) {
const localePage = `/${locale}${page === '/' ? '' : page}`
if (!ssgNotFoundPaths.includes(localePage)) {
finalPrerenderRoutes[localePage] = {
initialRevalidateSeconds:
exportConfig.initialPageRevalidationMap[localePage],
srcRoute: null,
dataRoute: path.posix.join(
'/_next/data',
buildId,
`${file}.json`
),
}
}
}
} else {
finalPrerenderRoutes[page] = {
initialRevalidateSeconds:
exportConfig.initialPageRevalidationMap[page],
srcRoute: null,
dataRoute: path.posix.join(
'/_next/data',
buildId,
`${file}.json`
),
}
}
// Set Page Revalidation Interval
const pageInfo = pageInfos.get(page)
if (pageInfo) {
pageInfo.initialRevalidateSeconds =
exportConfig.initialPageRevalidationMap[page]
pageInfos.set(page, pageInfo)
}
} else {
// For a dynamic SSG page, we did not copy its data exports and only
// copy the fallback HTML file (if present).
// We must also copy specific versions of this page as defined by
// `getStaticPaths` (additionalSsgPaths).
const extraRoutes = additionalSsgPaths.get(page) || []
for (const route of extraRoutes) {
const pageFile = normalizePagePath(route)
await moveExportedPage(
page,
route,
pageFile,
isSsg,
'html',
true
)
await moveExportedPage(
page,
route,
pageFile,
isSsg,
'json',
true
)
if (hasAmp) {
const ampPage = `${pageFile}.amp`
await moveExportedPage(
page,
ampPage,
ampPage,
isSsg,
'html',
true
)
await moveExportedPage(
page,
ampPage,
ampPage,
isSsg,
'json',
true
)
}
finalPrerenderRoutes[route] = {
initialRevalidateSeconds:
exportConfig.initialPageRevalidationMap[route],
srcRoute: page,
dataRoute: path.posix.join(
'/_next/data',
buildId,
`${normalizePagePath(route)}.json`
),
}
// Set route Revalidation Interval
const pageInfo = pageInfos.get(route)
if (pageInfo) {
pageInfo.initialRevalidateSeconds =
exportConfig.initialPageRevalidationMap[route]
pageInfos.set(route, pageInfo)
}
}
}
}
}
// remove temporary export folder
await recursiveDelete(exportOptions.outdir)
await promises.rmdir(exportOptions.outdir)
await promises.writeFile(
manifestPath,
JSON.stringify(pagesManifest, null, 2),
'utf8'
)
if (postBuildSpinner) postBuildSpinner.stopAndPersist()
console.log()
})
}
const analysisEnd = process.hrtime(analysisBegin)
telemetry.record(
eventBuildOptimize(pagePaths, {
durationInSeconds: analysisEnd[0],
staticPageCount: staticPages.size,
staticPropsPageCount: ssgPages.size,
serverPropsPageCount: serverPropsPages.size,
ssrPageCount:
pagePaths.length -
(staticPages.size + ssgPages.size + serverPropsPages.size),
hasStatic404: useStatic404,
hasReportWebVitals: namedExports?.includes('reportWebVitals') ?? false,
rewritesCount: rewrites.length,
headersCount: headers.length,
redirectsCount: redirects.length - 1, // reduce one for trailing slash
headersWithHasCount: headers.filter((r: any) => !!r.has).length,
rewritesWithHasCount: rewrites.filter((r: any) => !!r.has).length,
redirectsWithHasCount: redirects.filter((r: any) => !!r.has).length,
})
)
if (ssgPages.size > 0) {
const finalDynamicRoutes: PrerenderManifest['dynamicRoutes'] = {}
tbdPrerenderRoutes.forEach((tbdRoute) => {
const normalizedRoute = normalizePagePath(tbdRoute)
const dataRoute = path.posix.join(
'/_next/data',
buildId,
`${normalizedRoute}.json`
)
finalDynamicRoutes[tbdRoute] = {
routeRegex: normalizeRouteRegex(getRouteRegex(tbdRoute).re.source),
dataRoute,
fallback: ssgBlockingFallbackPages.has(tbdRoute)
? null
: ssgStaticFallbackPages.has(tbdRoute)
? `${normalizedRoute}.html`
: false,
dataRouteRegex: normalizeRouteRegex(
getRouteRegex(dataRoute.replace(/\.json$/, '')).re.source.replace(
/\(\?:\\\/\)\?\$$/,
'\\.json$'
)
),
}
})
const prerenderManifest: PrerenderManifest = {
version: 3,
routes: finalPrerenderRoutes,
dynamicRoutes: finalDynamicRoutes,
notFoundRoutes: ssgNotFoundPaths,
preview: previewProps,
}
await promises.writeFile(
path.join(distDir, PRERENDER_MANIFEST),
JSON.stringify(prerenderManifest),
'utf8'
)
await generateClientSsgManifest(prerenderManifest, {
distDir,
buildId,
locales: config.i18n?.locales || [],
})
} else {
const prerenderManifest: PrerenderManifest = {
version: 3,
routes: {},
dynamicRoutes: {},
preview: previewProps,
notFoundRoutes: [],
}
await promises.writeFile(
path.join(distDir, PRERENDER_MANIFEST),
JSON.stringify(prerenderManifest),
'utf8'
)
}
const images = { ...config.images }
const { deviceSizes, imageSizes } = images
images.sizes = [...deviceSizes, ...imageSizes]
await promises.writeFile(
path.join(distDir, IMAGES_MANIFEST),
JSON.stringify({
version: 1,
images,
}),
'utf8'
)
await promises.writeFile(
path.join(distDir, EXPORT_MARKER),
JSON.stringify({
version: 1,
hasExportPathMap: typeof config.exportPathMap === 'function',
exportTrailingSlash: config.trailingSlash === true,
isNextImageImported: isNextImageImported === true,
}),
'utf8'
)
await promises.unlink(path.join(distDir, EXPORT_DETAIL)).catch((err) => {
if (err.code === 'ENOENT') {
return Promise.resolve()
}
return Promise.reject(err)
})
staticPages.forEach((pg) => allStaticPages.add(pg))
pageInfos.forEach((info: PageInfo, key: string) => {
allPageInfos.set(key, info)
})
await nextBuildSpan.traceChild('print-tree-view').traceAsyncFn(() =>
printTreeView(Object.keys(mappedPages), allPageInfos, isLikeServerless, {
distPath: distDir,
buildId: buildId,
pagesDir,
useStatic404,
pageExtensions: config.pageExtensions,
buildManifest,
})
)
if (debugOutput) {
nextBuildSpan
.traceChild('print-custom-routes')
.traceFn(() => printCustomRoutes({ redirects, rewrites, headers }))
}
if (config.analyticsId) {
console.log(
chalk.bold.green('Next.js Analytics') +
' is enabled for this production build. ' +
"You'll receive a Real Experience Score computed by all of your visitors."
)
console.log('')
}
await nextBuildSpan
.traceChild('telemetry-flush')
.traceAsyncFn(() => telemetry.flush())
})
}
export type ClientSsgManifest = Set<string>
function generateClientSsgManifest(
prerenderManifest: PrerenderManifest,
{
buildId,
distDir,
locales,
}: { buildId: string; distDir: string; locales: string[] }
) {
const ssgPages: ClientSsgManifest = new Set<string>([
...Object.entries(prerenderManifest.routes)
// Filter out dynamic routes
.filter(([, { srcRoute }]) => srcRoute == null)
.map(([route]) => normalizeLocalePath(route, locales).pathname),
...Object.keys(prerenderManifest.dynamicRoutes),
])
const clientSsgManifestContent = `self.__SSG_MANIFEST=${devalue(
ssgPages
)};self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()`
writeFileSync(
path.join(distDir, CLIENT_STATIC_FILES_PATH, buildId, '_ssgManifest.js'),
clientSsgManifestContent
)
}