204028d6ab
* Add progress for analyzing and auto-prerendering * Add typing for tty-aware-progress and use stdout * Add fancier spinners * Update spinner and add handling for logs while spinning * Remove un-needed types package * Remove progress and combine analyzing/prerendering messages
277 lines
7.2 KiB
JavaScript
277 lines
7.2 KiB
JavaScript
import { cpus } from 'os'
|
|
import chalk from 'chalk'
|
|
import Worker from 'jest-worker'
|
|
import { promisify } from 'util'
|
|
import mkdirpModule from 'mkdirp'
|
|
import { resolve, join } from 'path'
|
|
import { API_ROUTE } from '../lib/constants'
|
|
import { existsSync, readFileSync } from 'fs'
|
|
import { recursiveCopy } from '../lib/recursive-copy'
|
|
import { recursiveDelete } from '../lib/recursive-delete'
|
|
import { formatAmpMessages } from '../build/output/index'
|
|
import loadConfig, {
|
|
isTargetLikeServerless
|
|
} from '../next-server/server/config'
|
|
import {
|
|
PHASE_EXPORT,
|
|
SERVER_DIRECTORY,
|
|
PAGES_MANIFEST,
|
|
CONFIG_FILE,
|
|
BUILD_ID_FILE,
|
|
CLIENT_PUBLIC_FILES_PATH,
|
|
CLIENT_STATIC_FILES_PATH
|
|
} from '../next-server/lib/constants'
|
|
import createSpinner from '../build/spinner'
|
|
|
|
const mkdirp = promisify(mkdirpModule)
|
|
|
|
const createProgress = (total, label = 'Exporting') => {
|
|
let curProgress = 0
|
|
let progressSpinner = createSpinner(`${label} (${curProgress}/${total})`, {
|
|
spinner: {
|
|
frames: [
|
|
'[ ]',
|
|
'[= ]',
|
|
'[== ]',
|
|
'[=== ]',
|
|
'[ ===]',
|
|
'[ ==]',
|
|
'[ =]',
|
|
'[ ]',
|
|
'[ =]',
|
|
'[ ==]',
|
|
'[ ===]',
|
|
'[====]',
|
|
'[=== ]',
|
|
'[== ]',
|
|
'[= ]'
|
|
],
|
|
interval: 80
|
|
}
|
|
})
|
|
|
|
return () => {
|
|
curProgress++
|
|
|
|
const newText = `${label} (${curProgress}/${total})`
|
|
if (progressSpinner) {
|
|
progressSpinner.text = newText
|
|
} else {
|
|
console.log(newText)
|
|
}
|
|
|
|
if (curProgress === total && progressSpinner) {
|
|
progressSpinner.stop()
|
|
console.log(newText)
|
|
}
|
|
}
|
|
}
|
|
|
|
export default async function (dir, options, configuration) {
|
|
function log (message) {
|
|
if (options.silent) return
|
|
console.log(message)
|
|
}
|
|
|
|
dir = resolve(dir)
|
|
const nextConfig = configuration || loadConfig(PHASE_EXPORT, dir)
|
|
const threads = options.threads || Math.max(cpus().length - 1, 1)
|
|
const distDir = join(dir, nextConfig.distDir)
|
|
const subFolders = nextConfig.exportTrailingSlash
|
|
|
|
if (!options.buildExport && nextConfig.target !== 'server') {
|
|
throw new Error(
|
|
'Cannot export when target is not server. https://err.sh/zeit/next.js/next-export-serverless'
|
|
)
|
|
}
|
|
|
|
log(`> using build directory: ${distDir}`)
|
|
|
|
if (!existsSync(distDir)) {
|
|
throw new Error(
|
|
`Build directory ${distDir} does not exist. Make sure you run "next build" before running "next start" or "next export".`
|
|
)
|
|
}
|
|
|
|
const buildId = readFileSync(join(distDir, BUILD_ID_FILE), 'utf8')
|
|
const pagesManifest =
|
|
!options.pages && require(join(distDir, SERVER_DIRECTORY, PAGES_MANIFEST))
|
|
|
|
const pages = options.pages || Object.keys(pagesManifest)
|
|
const defaultPathMap = {}
|
|
|
|
for (const page of pages) {
|
|
// _document and _app are not real pages
|
|
// _error is exported as 404.html later on
|
|
// API Routes are Node.js functions
|
|
if (
|
|
page === '/_document' ||
|
|
page === '/_app' ||
|
|
page === '/_error' ||
|
|
page.match(API_ROUTE)
|
|
) {
|
|
continue
|
|
}
|
|
|
|
defaultPathMap[page] = { page }
|
|
}
|
|
|
|
// Initialize the output directory
|
|
const outDir = options.outdir
|
|
await recursiveDelete(join(outDir))
|
|
await mkdirp(join(outDir, '_next', buildId))
|
|
|
|
// Copy static directory
|
|
if (existsSync(join(dir, 'static'))) {
|
|
log(' copying "static" directory')
|
|
await recursiveCopy(join(dir, 'static'), join(outDir, 'static'))
|
|
}
|
|
|
|
// Copy .next/static directory
|
|
if (existsSync(join(distDir, CLIENT_STATIC_FILES_PATH))) {
|
|
log(' copying "static build" directory')
|
|
await recursiveCopy(
|
|
join(distDir, CLIENT_STATIC_FILES_PATH),
|
|
join(outDir, '_next', CLIENT_STATIC_FILES_PATH)
|
|
)
|
|
}
|
|
|
|
// Get the exportPathMap from the config file
|
|
if (typeof nextConfig.exportPathMap !== 'function') {
|
|
console.log(
|
|
`> No "exportPathMap" found in "${CONFIG_FILE}". Generating map from "./pages"`
|
|
)
|
|
nextConfig.exportPathMap = async defaultMap => {
|
|
return defaultMap
|
|
}
|
|
}
|
|
|
|
// Start the rendering process
|
|
const renderOpts = {
|
|
dir,
|
|
buildId,
|
|
nextExport: true,
|
|
assetPrefix: nextConfig.assetPrefix.replace(/\/$/, ''),
|
|
distDir,
|
|
dev: false,
|
|
staticMarkup: false,
|
|
hotReloader: null,
|
|
canonicalBase: (nextConfig.amp && nextConfig.amp.canonicalBase) || '',
|
|
isModern: nextConfig.experimental.modern
|
|
}
|
|
|
|
const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig
|
|
|
|
if (Object.keys(publicRuntimeConfig).length > 0) {
|
|
renderOpts.runtimeConfig = publicRuntimeConfig
|
|
}
|
|
|
|
// We need this for server rendering the Link component.
|
|
global.__NEXT_DATA__ = {
|
|
nextExport: true
|
|
}
|
|
|
|
log(` launching ${threads} workers`)
|
|
const exportPathMap = await nextConfig.exportPathMap(defaultPathMap, {
|
|
dev: false,
|
|
dir,
|
|
outDir,
|
|
distDir,
|
|
buildId
|
|
})
|
|
if (!exportPathMap['/404']) {
|
|
exportPathMap['/404.html'] = exportPathMap['/404.html'] || {
|
|
page: '/_error'
|
|
}
|
|
}
|
|
const exportPaths = Object.keys(exportPathMap)
|
|
const filteredPaths = exportPaths.filter(
|
|
// Remove API routes
|
|
route => !exportPathMap[route].page.match(API_ROUTE)
|
|
)
|
|
const hasApiRoutes = exportPaths.length !== filteredPaths.length
|
|
|
|
// Warn if the user defines a path for an API page
|
|
if (hasApiRoutes) {
|
|
log(
|
|
chalk.yellow(
|
|
' API pages are not supported by next export. https://err.sh/zeit/next.js/api-routes-static-export'
|
|
)
|
|
)
|
|
}
|
|
|
|
const progress = !options.silent && createProgress(filteredPaths.length)
|
|
|
|
const ampValidations = {}
|
|
let hadValidationError = false
|
|
|
|
const publicDir = join(dir, CLIENT_PUBLIC_FILES_PATH)
|
|
// Copy public directory
|
|
if (
|
|
nextConfig.experimental &&
|
|
nextConfig.experimental.publicDirectory &&
|
|
existsSync(publicDir)
|
|
) {
|
|
log(' copying "public" directory')
|
|
await recursiveCopy(publicDir, outDir, {
|
|
filter (path) {
|
|
// Exclude paths used by pages
|
|
return !exportPathMap[path]
|
|
}
|
|
})
|
|
}
|
|
|
|
const worker = new Worker(require.resolve('./worker'), {
|
|
maxRetries: 0,
|
|
numWorkers: threads,
|
|
enableWorkerThreads: true,
|
|
exposedMethods: ['default']
|
|
})
|
|
|
|
worker.getStdout().pipe(process.stdout)
|
|
worker.getStderr().pipe(process.stderr)
|
|
|
|
let renderError = false
|
|
|
|
await Promise.all(
|
|
filteredPaths.map(async path => {
|
|
const result = await worker.default({
|
|
path,
|
|
pathMap: exportPathMap[path],
|
|
distDir,
|
|
buildId,
|
|
outDir,
|
|
renderOpts,
|
|
serverRuntimeConfig,
|
|
subFolders,
|
|
serverless: isTargetLikeServerless(nextConfig.target)
|
|
})
|
|
|
|
for (const validation of result.ampValidations || []) {
|
|
const { page, result } = validation
|
|
ampValidations[page] = result
|
|
hadValidationError |=
|
|
Array.isArray(result && result.errors) && result.errors.length > 0
|
|
}
|
|
renderError |= result.error
|
|
if (progress) progress()
|
|
})
|
|
)
|
|
|
|
worker.end()
|
|
|
|
if (Object.keys(ampValidations).length) {
|
|
console.log(formatAmpMessages(ampValidations))
|
|
}
|
|
if (hadValidationError) {
|
|
throw new Error(
|
|
`AMP Validation caused the export to fail. https://err.sh/zeit/next.js/amp-export-validation`
|
|
)
|
|
}
|
|
|
|
if (renderError) {
|
|
throw new Error(`Export encountered errors`)
|
|
}
|
|
// Add an empty line to the console for the better readability.
|
|
log('')
|
|
}
|