87ed37d51c
This insures we add entries for each locale version of a non-dynamic SSG page since they can have unique revalidate values. This requires a version bump in the `prerender-manifest` since the static routes now contain additional values which need to be handled separately. Fixes: https://github.com/vercel/next.js/issues/21568
1452 lines
47 KiB
TypeScript
1452 lines
47 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 {
|
|
PAGES_404_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,
|
|
} 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 { traceAsyncFn, traceFn, tracer } from './tracer'
|
|
import {
|
|
collectPages,
|
|
detectConflictingPaths,
|
|
getJsPageSizeInKb,
|
|
getNamedExports,
|
|
hasCustomGetInitialProps,
|
|
isPageStatic,
|
|
PageInfo,
|
|
printCustomRoutes,
|
|
printTreeView,
|
|
} from './utils'
|
|
import getBaseWebpackConfig from './webpack-config'
|
|
import { PagesManifest } from './webpack/plugins/pages-manifest-plugin'
|
|
import { writeBuildId } from './write-build-id'
|
|
import opentelemetryApi from '@opentelemetry/api'
|
|
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 span = tracer.startSpan('next-build')
|
|
return traceAsyncFn(span, async () => {
|
|
// attempt to load global env values so they are available in next.config.js
|
|
const { loadedEnvFiles } = traceFn(tracer.startSpan('load-dotenv'), () =>
|
|
loadEnvConfig(dir, false, Log)
|
|
)
|
|
|
|
const config = traceFn(tracer.startSpan('load-next-config'), () =>
|
|
loadConfig(PHASE_PRODUCTION_BUILD, dir, conf)
|
|
)
|
|
const { target } = config
|
|
const buildId = await traceAsyncFn(
|
|
tracer.startSpan('generate-buildid'),
|
|
() => generateBuildId(config.generateBuildId, nanoid)
|
|
)
|
|
const distDir = path.join(dir, config.distDir)
|
|
|
|
const {
|
|
headers,
|
|
rewrites,
|
|
redirects,
|
|
} = await traceAsyncFn(tracer.startSpan('load-custom-routes'), () =>
|
|
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 buildSpinner = createSpinner({
|
|
prefixText: `${Log.prefixes.info} Creating an optimized production build`,
|
|
})
|
|
|
|
const telemetry = new Telemetry({ distDir })
|
|
|
|
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)
|
|
)
|
|
|
|
const ignoreTypeScriptErrors = Boolean(config.typescript?.ignoreBuildErrors)
|
|
await traceAsyncFn(tracer.startSpan('verify-typescript-setup'), () =>
|
|
verifyTypeScriptSetup(dir, pagesDir, !ignoreTypeScriptErrors)
|
|
)
|
|
|
|
const isLikeServerless = isTargetLikeServerless(target)
|
|
|
|
const pagePaths: string[] = await traceAsyncFn(
|
|
tracer.startSpan('collect-pages'),
|
|
() => 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 = traceFn(tracer.startSpan('create-pages-mapping'), () =>
|
|
createPagesMapping(pagePaths, config.pageExtensions)
|
|
)
|
|
const entrypoints = traceFn(tracer.startSpan('create-entrypoints'), () =>
|
|
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 traceAsyncFn(
|
|
tracer.startSpan('public-dir-conflict-check'),
|
|
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
|
|
}
|
|
} = traceFn(tracer.startSpan('generate-routes-manifest'), () => ({
|
|
version: 3,
|
|
pages404: true,
|
|
basePath: config.basePath,
|
|
redirects: redirects.map((r) => buildCustomRoute(r, 'redirect')),
|
|
rewrites: rewrites.map((r) => buildCustomRoute(r, 'rewrite')),
|
|
headers: headers.map((r) => 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 traceAsyncFn(
|
|
tracer.startSpan('create-distdir'),
|
|
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 traceAsyncFn(tracer.startSpan('write-routes-manifest'), () =>
|
|
promises.writeFile(
|
|
routesManifestPath,
|
|
JSON.stringify(routesManifest),
|
|
'utf8'
|
|
)
|
|
)
|
|
|
|
const manifestPath = path.join(
|
|
distDir,
|
|
isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
|
|
PAGES_MANIFEST
|
|
)
|
|
|
|
const requiredServerFiles = traceFn(
|
|
tracer.startSpan('generate-required-server-files'),
|
|
() => ({
|
|
version: 1,
|
|
config: {
|
|
...config,
|
|
compress: false,
|
|
configFile: undefined,
|
|
},
|
|
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: [
|
|
path.relative(
|
|
dir,
|
|
path.join(path.dirname(require.resolve('sharp')), '**/*')
|
|
),
|
|
],
|
|
})
|
|
)
|
|
|
|
const configs = await traceAsyncFn(
|
|
tracer.startSpan('generate-webpack-config'),
|
|
() =>
|
|
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: [] }
|
|
// TODO: why do we need this?? https://github.com/vercel/next.js/issues/8253
|
|
if (isLikeServerless) {
|
|
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 traceAsyncFn(
|
|
tracer.startSpan('run-webpack-compiler'),
|
|
() => runCompiler(configs)
|
|
)
|
|
}
|
|
|
|
const webpackBuildEnd = process.hrtime(webpackBuildStart)
|
|
if (buildSpinner) {
|
|
buildSpinner.stopAndPersist()
|
|
}
|
|
|
|
result = traceFn(tracer.startSpan('format-webpack-messages'), () =>
|
|
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 = tracer.startSpan('static-check')
|
|
const { hasNonStaticErrorPage } = await traceAsyncFn(
|
|
staticCheckSpan,
|
|
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 nonStaticErrorPage = await traceAsyncFn(
|
|
tracer.startSpan('check-static-error-page'),
|
|
async () =>
|
|
hasCustomErrorPage &&
|
|
(await hasCustomGetInitialProps(
|
|
getPagePath('/_error', distDir, isLikeServerless),
|
|
runtimeEnvConfig,
|
|
false
|
|
))
|
|
)
|
|
|
|
await Promise.all(
|
|
pageKeys.map(async (page) => {
|
|
return traceAsyncFn(
|
|
tracer.startSpan('check-page', { attributes: { page } }),
|
|
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) {
|
|
const serverBundle = getPagePath(
|
|
page,
|
|
distDir,
|
|
isLikeServerless
|
|
)
|
|
|
|
if (customAppGetInitialProps === undefined) {
|
|
customAppGetInitialProps = await hasCustomGetInitialProps(
|
|
isLikeServerless
|
|
? serverBundle
|
|
: getPagePath('/_app', distDir, isLikeServerless),
|
|
runtimeEnvConfig,
|
|
true
|
|
)
|
|
|
|
namedExports = getNamedExports(
|
|
isLikeServerless
|
|
? serverBundle
|
|
: getPagePath('/_app', 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'
|
|
)
|
|
}
|
|
}
|
|
|
|
try {
|
|
let workerResult = await traceAsyncFn(
|
|
tracer.startSpan('is-page-static'),
|
|
() => {
|
|
const spanContext = {}
|
|
|
|
opentelemetryApi.propagation.inject(
|
|
opentelemetryApi.context.active(),
|
|
spanContext
|
|
)
|
|
return staticCheckWorkers.isPageStatic(
|
|
page,
|
|
serverBundle,
|
|
runtimeEnvConfig,
|
|
config.i18n?.locales,
|
|
config.i18n?.defaultLocale,
|
|
spanContext
|
|
)
|
|
}
|
|
)
|
|
|
|
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_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)
|
|
}
|
|
}
|
|
} 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('@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)
|
|
|
|
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
|
|
|
|
await traceAsyncFn(tracer.startSpan('static-generation'), async () => {
|
|
if (staticPages.size > 0 || ssgPages.size > 0 || useStatic404) {
|
|
const combinedPages = [...staticPages, ...ssgPages]
|
|
|
|
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 (i18n) {
|
|
for (const page of [
|
|
...staticPages,
|
|
...ssgPages,
|
|
...(useStatic404 ? ['/404'] : []),
|
|
]) {
|
|
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
|
|
},
|
|
trailingSlash: false,
|
|
}
|
|
|
|
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 traceAsyncFn(
|
|
tracer.startSpan('move-exported-page'),
|
|
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) {
|
|
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')
|
|
}
|
|
|
|
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
|
|
})
|
|
)
|
|
|
|
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 traceAsyncFn(tracer.startSpan('print-tree-view'), () =>
|
|
printTreeView(Object.keys(mappedPages), allPageInfos, isLikeServerless, {
|
|
distPath: distDir,
|
|
buildId: buildId,
|
|
pagesDir,
|
|
useStatic404,
|
|
pageExtensions: config.pageExtensions,
|
|
buildManifest,
|
|
})
|
|
)
|
|
|
|
if (debugOutput) {
|
|
traceFn(tracer.startSpan('print-custom-routes'), () =>
|
|
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 traceAsyncFn(tracer.startSpan('telemetry-flush'), () =>
|
|
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
|
|
)
|
|
}
|