2018-12-12 13:59:11 +01:00
|
|
|
import { cpus } from 'os'
|
2019-09-05 01:56:11 +02:00
|
|
|
import chalk from 'chalk'
|
|
|
|
import Worker from 'jest-worker'
|
|
|
|
import { promisify } from 'util'
|
2019-02-22 17:33:28 +01:00
|
|
|
import mkdirpModule from 'mkdirp'
|
2018-12-12 13:59:11 +01:00
|
|
|
import { resolve, join } from 'path'
|
2019-09-05 01:56:11 +02:00
|
|
|
import { API_ROUTE } from '../lib/constants'
|
2018-12-12 13:59:11 +01:00
|
|
|
import { existsSync, readFileSync } from 'fs'
|
2019-09-05 01:56:11 +02:00
|
|
|
import { recursiveCopy } from '../lib/recursive-copy'
|
|
|
|
import { recursiveDelete } from '../lib/recursive-delete'
|
|
|
|
import { formatAmpMessages } from '../build/output/index'
|
2019-08-06 00:26:20 +02:00
|
|
|
import loadConfig, {
|
|
|
|
isTargetLikeServerless
|
2019-09-04 16:00:54 +02:00
|
|
|
} from '../next-server/server/config'
|
2019-05-29 13:57:26 +02:00
|
|
|
import {
|
|
|
|
PHASE_EXPORT,
|
|
|
|
SERVER_DIRECTORY,
|
|
|
|
PAGES_MANIFEST,
|
|
|
|
CONFIG_FILE,
|
|
|
|
BUILD_ID_FILE,
|
|
|
|
CLIENT_PUBLIC_FILES_PATH,
|
|
|
|
CLIENT_STATIC_FILES_PATH
|
2019-09-04 16:00:54 +02:00
|
|
|
} from '../next-server/lib/constants'
|
2019-09-16 17:37:00 +02:00
|
|
|
import createSpinner from '../build/spinner'
|
2019-02-22 17:33:28 +01:00
|
|
|
|
|
|
|
const mkdirp = promisify(mkdirpModule)
|
2017-05-08 00:47:40 +02:00
|
|
|
|
2019-09-16 17:37:00 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-27 20:48:46 +02:00
|
|
|
export default async function (dir, options, configuration) {
|
2018-09-04 16:01:50 +02:00
|
|
|
function log (message) {
|
|
|
|
if (options.silent) return
|
|
|
|
console.log(message)
|
|
|
|
}
|
|
|
|
|
2017-05-08 08:10:26 +02:00
|
|
|
dir = resolve(dir)
|
2018-06-04 11:38:46 +02:00
|
|
|
const nextConfig = configuration || loadConfig(PHASE_EXPORT, dir)
|
2018-12-12 13:59:11 +01:00
|
|
|
const threads = options.threads || Math.max(cpus().length - 1, 1)
|
2018-06-04 15:45:39 +02:00
|
|
|
const distDir = join(dir, nextConfig.distDir)
|
2019-07-03 19:25:44 +02:00
|
|
|
const subFolders = nextConfig.exportTrailingSlash
|
2017-05-08 00:47:40 +02:00
|
|
|
|
2019-05-29 13:57:26 +02:00
|
|
|
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'
|
|
|
|
)
|
|
|
|
}
|
2019-02-26 22:48:30 +01:00
|
|
|
|
2018-06-04 15:45:39 +02:00
|
|
|
log(`> using build directory: ${distDir}`)
|
2017-05-11 18:23:08 +02:00
|
|
|
|
2018-06-04 15:45:39 +02:00
|
|
|
if (!existsSync(distDir)) {
|
2019-05-29 13:57:26 +02:00
|
|
|
throw new Error(
|
|
|
|
`Build directory ${distDir} does not exist. Make sure you run "next build" before running "next start" or "next export".`
|
|
|
|
)
|
2017-05-08 00:47:40 +02:00
|
|
|
}
|
|
|
|
|
2018-06-04 15:45:39 +02:00
|
|
|
const buildId = readFileSync(join(distDir, BUILD_ID_FILE), 'utf8')
|
2019-05-29 13:57:26 +02:00
|
|
|
const pagesManifest =
|
|
|
|
!options.pages && require(join(distDir, SERVER_DIRECTORY, PAGES_MANIFEST))
|
2018-03-30 15:08:09 +02:00
|
|
|
|
2019-05-22 18:36:53 +02:00
|
|
|
const pages = options.pages || Object.keys(pagesManifest)
|
2018-03-30 15:08:09 +02:00
|
|
|
const defaultPathMap = {}
|
|
|
|
|
|
|
|
for (const page of pages) {
|
Export 404 even if undefined in exportPathMap (#6912)
This PR adds the `_error`-page as an `404.html`-export, even when it is not explicitly defined in a custom `exportPathMap`.
It also fixes two false negative tests related to this. Previously the tests were matching the fallback 404-page from the test-server, rather than the `404.html`-page from next, which was actually not being generated. The test server is also not set up to serve `/404.html` as `/404` which the tests now reflect.
**Caveat**
In its current state, this PR removes `/404.html` from the `defaultPathMap` passed to the custom `exportPathMap`-functions, since it instead adds it after that function is run. While it is possible that someone is relying on this to exist, it is to my knowledge undocumented and also unlikely to be used for anything but merging it into the custom pathMap.
Since this would now merge `undefined` which would result in it being added later on anyway, I deemed it safe, but would be happy to undo that part of the PR if necessary as it was only cleanup.
**Examples**
As a way to demonstrate what this PR does, this is how examples changed:
* `basic-export` - Behaviour is unchanged, still has a `404.html`
* `with-static-export` - Now has a `404.html`
2019-04-21 22:24:28 +02:00
|
|
|
// _document and _app are not real pages
|
|
|
|
// _error is exported as 404.html later on
|
2019-08-12 00:37:20 +02:00
|
|
|
// API Routes are Node.js functions
|
2019-08-06 00:46:02 +02:00
|
|
|
if (
|
|
|
|
page === '/_document' ||
|
|
|
|
page === '/_app' ||
|
|
|
|
page === '/_error' ||
|
2019-08-12 00:37:20 +02:00
|
|
|
page.match(API_ROUTE)
|
2019-08-06 00:46:02 +02:00
|
|
|
) {
|
2018-08-27 12:28:54 +02:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-03-30 15:08:09 +02:00
|
|
|
defaultPathMap[page] = { page }
|
|
|
|
}
|
2017-05-08 00:47:40 +02:00
|
|
|
|
|
|
|
// Initialize the output directory
|
2017-06-08 03:39:45 +02:00
|
|
|
const outDir = options.outdir
|
2019-03-05 14:01:42 +01:00
|
|
|
await recursiveDelete(join(outDir))
|
2017-05-08 00:47:40 +02:00
|
|
|
await mkdirp(join(outDir, '_next', buildId))
|
|
|
|
|
2017-05-14 02:11:13 +02:00
|
|
|
// Copy static directory
|
|
|
|
if (existsSync(join(dir, 'static'))) {
|
|
|
|
log(' copying "static" directory')
|
2019-06-06 12:33:11 +02:00
|
|
|
await recursiveCopy(join(dir, 'static'), join(outDir, 'static'))
|
2017-05-14 02:11:13 +02:00
|
|
|
}
|
|
|
|
|
2018-02-07 11:54:07 +01:00
|
|
|
// Copy .next/static directory
|
2018-07-25 13:45:42 +02:00
|
|
|
if (existsSync(join(distDir, CLIENT_STATIC_FILES_PATH))) {
|
2018-02-07 11:54:07 +01:00
|
|
|
log(' copying "static build" directory')
|
2019-06-06 12:33:11 +02:00
|
|
|
await recursiveCopy(
|
2018-07-25 13:45:42 +02:00
|
|
|
join(distDir, CLIENT_STATIC_FILES_PATH),
|
|
|
|
join(outDir, '_next', CLIENT_STATIC_FILES_PATH)
|
2018-02-07 11:54:07 +01:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2018-06-04 11:38:46 +02:00
|
|
|
// Get the exportPathMap from the config file
|
2018-02-26 12:03:27 +01:00
|
|
|
if (typeof nextConfig.exportPathMap !== 'function') {
|
2019-05-29 13:57:26 +02:00
|
|
|
console.log(
|
|
|
|
`> No "exportPathMap" found in "${CONFIG_FILE}". Generating map from "./pages"`
|
|
|
|
)
|
|
|
|
nextConfig.exportPathMap = async defaultMap => {
|
2018-03-30 15:08:09 +02:00
|
|
|
return defaultMap
|
|
|
|
}
|
2017-05-09 03:20:50 +02:00
|
|
|
}
|
|
|
|
|
2017-05-08 08:10:26 +02:00
|
|
|
// Start the rendering process
|
|
|
|
const renderOpts = {
|
|
|
|
dir,
|
|
|
|
buildId,
|
2017-05-09 03:20:50 +02:00
|
|
|
nextExport: true,
|
2018-02-26 12:03:27 +01:00
|
|
|
assetPrefix: nextConfig.assetPrefix.replace(/\/$/, ''),
|
2018-06-04 15:45:39 +02:00
|
|
|
distDir,
|
2017-05-08 08:10:26 +02:00
|
|
|
dev: false,
|
|
|
|
staticMarkup: false,
|
2019-05-29 02:32:18 +02:00
|
|
|
hotReloader: null,
|
2019-07-25 04:16:32 +02:00
|
|
|
canonicalBase: (nextConfig.amp && nextConfig.amp.canonicalBase) || '',
|
|
|
|
isModern: nextConfig.experimental.modern
|
2018-02-26 12:03:27 +01:00
|
|
|
}
|
|
|
|
|
2019-02-19 22:45:07 +01:00
|
|
|
const { serverRuntimeConfig, publicRuntimeConfig } = nextConfig
|
2018-02-27 17:50:14 +01:00
|
|
|
|
2019-07-10 16:43:04 +02:00
|
|
|
if (Object.keys(publicRuntimeConfig).length > 0) {
|
2018-02-27 17:50:14 +01:00
|
|
|
renderOpts.runtimeConfig = publicRuntimeConfig
|
2017-05-08 08:10:26 +02:00
|
|
|
}
|
|
|
|
|
2017-05-09 03:20:50 +02:00
|
|
|
// We need this for server rendering the Link component.
|
|
|
|
global.__NEXT_DATA__ = {
|
|
|
|
nextExport: true
|
2017-05-08 08:10:26 +02:00
|
|
|
}
|
|
|
|
|
2019-09-05 01:56:11 +02:00
|
|
|
log(` launching ${threads} workers`)
|
2019-05-29 13:57:26 +02:00
|
|
|
const exportPathMap = await nextConfig.exportPathMap(defaultPathMap, {
|
|
|
|
dev: false,
|
|
|
|
dir,
|
|
|
|
outDir,
|
|
|
|
distDir,
|
|
|
|
buildId
|
|
|
|
})
|
2019-08-19 17:16:00 +02:00
|
|
|
if (!exportPathMap['/404']) {
|
2019-09-04 16:00:54 +02:00
|
|
|
exportPathMap['/404.html'] = exportPathMap['/404.html'] || {
|
|
|
|
page: '/_error'
|
|
|
|
}
|
2019-08-19 17:16:00 +02:00
|
|
|
}
|
2018-05-11 14:52:39 +02:00
|
|
|
const exportPaths = Object.keys(exportPathMap)
|
2019-08-12 00:37:20 +02:00
|
|
|
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'
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
2018-05-11 14:52:39 +02:00
|
|
|
|
2019-08-12 00:37:20 +02:00
|
|
|
const progress = !options.silent && createProgress(filteredPaths.length)
|
2017-05-09 03:53:08 +02:00
|
|
|
|
2019-03-26 22:21:27 +01:00
|
|
|
const ampValidations = {}
|
|
|
|
let hadValidationError = false
|
|
|
|
|
2019-05-03 18:57:47 +02:00
|
|
|
const publicDir = join(dir, CLIENT_PUBLIC_FILES_PATH)
|
|
|
|
// Copy public directory
|
2019-07-16 01:33:20 +02:00
|
|
|
if (
|
|
|
|
nextConfig.experimental &&
|
|
|
|
nextConfig.experimental.publicDirectory &&
|
|
|
|
existsSync(publicDir)
|
|
|
|
) {
|
2019-05-03 18:57:47 +02:00
|
|
|
log(' copying "public" directory')
|
2019-06-06 12:33:11 +02:00
|
|
|
await recursiveCopy(publicDir, outDir, {
|
2019-05-29 13:57:26 +02:00
|
|
|
filter (path) {
|
|
|
|
// Exclude paths used by pages
|
2019-06-06 12:33:11 +02:00
|
|
|
return !exportPathMap[path]
|
2019-05-03 18:57:47 +02:00
|
|
|
}
|
2019-05-29 13:57:26 +02:00
|
|
|
})
|
2019-05-03 18:57:47 +02:00
|
|
|
}
|
2019-09-05 01:56:11 +02:00
|
|
|
|
|
|
|
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
|
2019-05-03 18:57:47 +02:00
|
|
|
|
2018-12-12 13:59:11 +01:00
|
|
|
await Promise.all(
|
2019-09-05 01:56:11 +02:00
|
|
|
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()
|
|
|
|
})
|
2018-12-12 13:59:11 +01:00
|
|
|
)
|
2017-05-11 18:23:08 +02:00
|
|
|
|
2019-09-05 01:56:11 +02:00
|
|
|
worker.end()
|
2019-05-22 18:36:53 +02:00
|
|
|
|
2019-03-26 22:21:27 +01:00
|
|
|
if (Object.keys(ampValidations).length) {
|
|
|
|
console.log(formatAmpMessages(ampValidations))
|
|
|
|
}
|
|
|
|
if (hadValidationError) {
|
2019-05-29 13:57:26 +02:00
|
|
|
throw new Error(
|
|
|
|
`AMP Validation caused the export to fail. https://err.sh/zeit/next.js/amp-export-validation`
|
|
|
|
)
|
2019-03-26 22:21:27 +01:00
|
|
|
}
|
|
|
|
|
2019-09-05 01:56:11 +02:00
|
|
|
if (renderError) {
|
|
|
|
throw new Error(`Export encountered errors`)
|
|
|
|
}
|
2017-05-11 18:23:08 +02:00
|
|
|
// Add an empty line to the console for the better readability.
|
|
|
|
log('')
|
2017-05-08 00:47:40 +02:00
|
|
|
}
|