2020-03-29 00:43:52 +01:00
import chalk from 'next/dist/compiled/chalk'
2020-03-29 18:21:53 +02:00
import findUp from 'next/dist/compiled/find-up'
2019-12-13 20:30:22 +01:00
import {
2020-05-02 06:10:19 +02:00
promises ,
2019-12-13 20:30:22 +01:00
existsSync ,
2020-03-25 09:22:34 +01:00
exists as existsOrig ,
2019-12-13 20:30:22 +01:00
readFileSync ,
writeFileSync ,
} from 'fs'
2019-09-05 01:56:11 +02:00
import Worker from 'jest-worker'
2019-10-04 17:26:44 +02:00
import { cpus } from 'os'
2019-10-31 21:26:47 +01:00
import { dirname , join , resolve , sep } from 'path'
2019-10-04 17:26:44 +02:00
import { promisify } from 'util'
import { AmpPageStatus , formatAmpMessages } from '../build/output/index'
import createSpinner from '../build/spinner'
2019-09-05 01:56:11 +02:00
import { API_ROUTE } from '../lib/constants'
import { recursiveCopy } from '../lib/recursive-copy'
import { recursiveDelete } from '../lib/recursive-delete'
2019-05-29 13:57:26 +02:00
import {
BUILD_ID_FILE ,
2019-10-04 17:26:44 +02:00
CLIENT_PUBLIC_FILES_PATH ,
CLIENT_STATIC_FILES_PATH ,
CONFIG_FILE ,
2019-12-13 20:30:22 +01:00
EXPORT_DETAIL ,
2019-10-04 17:26:44 +02:00
PAGES_MANIFEST ,
PHASE_EXPORT ,
2019-09-24 10:50:04 +02:00
PRERENDER_MANIFEST ,
SERVERLESS_DIRECTORY ,
2019-12-13 20:30:22 +01:00
SERVER_DIRECTORY ,
2019-09-04 16:00:54 +02:00
} from '../next-server/lib/constants'
2019-10-04 17:26:44 +02:00
import loadConfig , {
isTargetLikeServerless ,
} from '../next-server/server/config'
2020-02-14 21:42:44 +01:00
import { eventCliSession } from '../telemetry/events'
2019-10-10 19:18:07 +02:00
import { Telemetry } from '../telemetry/storage'
2020-02-05 22:10:39 +01:00
import { normalizePagePath } from '../next-server/server/normalize-page-path'
2020-03-26 13:32:41 +01:00
import { loadEnvConfig } from '../lib/load-env-config'
2019-02-22 17:33:28 +01:00
2020-03-25 09:22:34 +01:00
const exists = promisify ( existsOrig )
2017-05-08 00:47:40 +02:00
2019-10-04 17:26:44 +02:00
const createProgress = ( total : number , label = 'Exporting' ) = > {
2019-09-16 17:37:00 +02:00
let curProgress = 0
let progressSpinner = createSpinner ( ` ${ label } ( ${ curProgress } / ${ total } ) ` , {
spinner : {
frames : [
'[ ]' ,
'[= ]' ,
'[== ]' ,
'[=== ]' ,
'[ ===]' ,
'[ ==]' ,
'[ =]' ,
'[ ]' ,
'[ =]' ,
'[ ==]' ,
'[ ===]' ,
'[====]' ,
'[=== ]' ,
'[== ]' ,
2019-10-04 17:26:44 +02:00
'[= ]' ,
2019-09-16 17:37:00 +02:00
] ,
2019-10-04 17:26:44 +02:00
interval : 80 ,
} ,
2019-09-16 17:37:00 +02:00
} )
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 )
}
}
}
2019-10-04 17:26:44 +02:00
type ExportPathMap = {
[ page : string ] : { page : string ; query ? : { [ key : string ] : string } }
}
export default async function (
dir : string ,
options : any ,
configuration? : any
) : Promise < void > {
function log ( message : string ) {
if ( options . silent ) {
return
}
2018-09-04 16:01:50 +02:00
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-12-03 17:18:58 +01:00
const telemetry = options . buildExport ? null : new Telemetry ( { distDir } )
if ( telemetry ) {
2019-11-08 18:03:50 +01:00
telemetry . record (
2020-02-14 21:42:44 +01:00
eventCliSession ( PHASE_EXPORT , distDir , {
2019-11-08 18:03:50 +01:00
cliCommand : 'export' ,
isSrcDir : null ,
hasNowJson : ! ! ( await findUp ( 'now.json' , { cwd : dir } ) ) ,
isCustomServer : null ,
} )
)
2019-10-03 16:21:15 +02:00
}
2019-07-03 19:25:44 +02:00
const subFolders = nextConfig . exportTrailingSlash
2019-09-24 10:50:04 +02:00
const isLikeServerless = nextConfig . target !== 'server'
2017-05-08 00:47:40 +02: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 =
2019-12-14 07:31:48 +01:00
! options . pages &&
require ( join (
distDir ,
isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY ,
PAGES_MANIFEST
) )
2018-03-30 15:08:09 +02:00
2019-09-24 10:50:04 +02:00
let prerenderManifest
try {
prerenderManifest = require ( join ( distDir , PRERENDER_MANIFEST ) )
} catch ( _ ) { }
const distPagesDir = join (
distDir ,
isLikeServerless
? SERVERLESS_DIRECTORY
: join ( SERVER_DIRECTORY , 'static' , buildId ) ,
'pages'
)
2019-05-22 18:36:53 +02:00
const pages = options . pages || Object . keys ( pagesManifest )
2019-10-04 17:26:44 +02:00
const defaultPathMap : ExportPathMap = { }
2020-03-24 18:38:22 +01:00
let hasApiRoutes = false
2018-03-30 15:08:09 +02:00
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
2020-03-24 18:38:22 +01:00
if ( page . match ( API_ROUTE ) ) {
hasApiRoutes = true
continue
}
if ( page === '/_document' || page === '/_app' || page === '/_error' ) {
2018-08-27 12:28:54 +02:00
continue
}
2019-11-22 02:20:19 +01:00
// iSSG pages that are dynamic should not export templated version by
// default. In most cases, this would never work. There is no server that
// could run `getStaticProps`. If users make their page work lazily, they
// can manually add it to the `exportPathMap`.
2020-01-08 17:30:53 +01:00
if ( prerenderManifest ? . dynamicRoutes [ page ] ) {
2019-11-22 02:20:19 +01: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-10-06 13:44:03 +02:00
if ( outDir === join ( dir , 'public' ) ) {
throw new Error (
` The 'public' directory is reserved in Next.js and can not be used as the export out directory. https://err.sh/zeit/next.js/can-not-output-to-public `
)
}
2019-03-05 14:01:42 +01:00
await recursiveDelete ( join ( outDir ) )
2020-05-02 06:10:19 +02:00
await promises . mkdir ( join ( outDir , '_next' , buildId ) , { recursive : true } )
2017-05-08 00:47:40 +02:00
2019-12-13 20:30:22 +01:00
writeFileSync (
join ( distDir , EXPORT_DETAIL ) ,
JSON . stringify ( {
version : 1 ,
outDirectory : outDir ,
success : false ,
} ) ,
'utf8'
)
2017-05-14 02:11:13 +02:00
// Copy static directory
2019-10-02 15:19:23 +02:00
if ( ! options . buildExport && existsSync ( join ( dir , 'static' ) ) ) {
2017-05-14 02:11:13 +02:00
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" `
)
2019-10-04 17:26:44 +02:00
nextConfig . exportPathMap = async ( defaultMap : ExportPathMap ) = > {
2018-03-30 15:08:09 +02:00
return defaultMap
}
2017-05-09 03:20:50 +02:00
}
2020-03-31 17:53:50 +02:00
loadEnvConfig ( dir )
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 ,
2020-04-14 09:50:39 +02:00
basePath : nextConfig.experimental.basePath ,
2020-01-08 17:30:53 +01:00
canonicalBase : nextConfig.amp?.canonicalBase || '' ,
2019-10-04 17:26:44 +02:00
isModern : nextConfig.experimental.modern ,
2020-02-02 19:02:56 +01:00
ampValidatorPath : nextConfig.experimental.amp?.validator || undefined ,
2020-03-24 09:31:04 +01:00
ampSkipValidation : nextConfig.experimental.amp?.skipValidation || false ,
ampOptimizerConfig : nextConfig.experimental.amp?.optimizer || undefined ,
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 ) {
2019-10-04 17:26:44 +02:00
; ( renderOpts as any ) . 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.
2019-10-04 17:26:44 +02:00
; ( global as any ) . __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 ,
2019-10-04 17:26:44 +02:00
buildId ,
2019-05-29 13:57:26 +02:00
} )
2020-01-15 21:18:31 +01:00
if ( ! exportPathMap [ '/404' ] && ! exportPathMap [ '/404.html' ] ) {
exportPathMap [ '/404' ] = exportPathMap [ '/404.html' ] = {
2019-10-04 17:26:44 +02:00
page : '/_error' ,
2019-09-04 16:00:54 +02:00
}
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 )
)
2020-03-24 18:38:22 +01:00
if ( filteredPaths . length !== exportPaths . length ) {
hasApiRoutes = true
}
2019-08-12 00:37:20 +02:00
// Warn if the user defines a path for an API page
if ( hasApiRoutes ) {
log (
2020-03-24 18:38:22 +01:00
chalk . bold . red ( ` Attention ` ) +
': ' +
chalk . yellow (
` Statically exporting a Next.js application via \` next export \` disables API routes. `
) +
` \ n ` +
chalk . yellow (
` This command is meant for static-only hosts, and is ` +
' ' +
chalk . bold ( ` not necessary to make your application static. ` )
) +
` \ n ` +
chalk . yellow (
` Pages in your application without server-side data dependencies will be automatically statically exported by \` next build \` , including pages powered by \` getStaticProps \` . `
) +
` \ nLearn more: https://err.sh/zeit/next.js/api-routes-static-export `
2019-08-12 00:37:20 +02:00
)
}
2018-05-11 14:52:39 +02:00
2019-08-12 00:37:20 +02:00
const progress = ! options . silent && createProgress ( filteredPaths . length )
2020-01-15 02:22:15 +01:00
const pagesDataDir = options . buildExport
2019-10-10 19:07:51 +02:00
? outDir
: join ( outDir , '_next/data' , buildId )
2017-05-09 03:53:08 +02:00
2019-10-04 17:26:44 +02:00
const ampValidations : AmpPageStatus = { }
2019-03-26 22:21:27 +01:00
let hadValidationError = false
2019-05-03 18:57:47 +02:00
const publicDir = join ( dir , CLIENT_PUBLIC_FILES_PATH )
// Copy public directory
2019-10-06 13:44:03 +02:00
if ( ! options . buildExport && 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-10-04 17:26:44 +02:00
filter ( path ) {
2019-05-29 13:57:26 +02:00
// Exclude paths used by pages
2019-06-06 12:33:11 +02:00
return ! exportPathMap [ path ]
2019-10-04 17:26:44 +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
2019-10-04 17:26:44 +02:00
const worker : Worker & { default : Function } = new Worker (
require . resolve ( './worker' ) ,
{
maxRetries : 0 ,
numWorkers : threads ,
2019-10-29 01:01:25 +01:00
enableWorkerThreads : nextConfig.experimental.workerThreads ,
2019-10-04 17:26:44 +02:00
exposedMethods : [ 'default' ] ,
}
) as any
2019-09-05 01:56:11 +02:00
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 ,
2020-01-15 02:22:15 +01:00
pagesDataDir ,
2019-09-05 01:56:11 +02:00
renderOpts ,
serverRuntimeConfig ,
subFolders ,
2019-09-24 10:50:04 +02:00
buildExport : options.buildExport ,
2019-10-04 17:26:44 +02:00
serverless : isTargetLikeServerless ( nextConfig . target ) ,
2019-09-05 01:56:11 +02:00
} )
for ( const validation of result . ampValidations || [ ] ) {
const { page , result } = validation
ampValidations [ page ] = result
2019-10-04 17:26:44 +02:00
hadValidationError =
hadValidationError ||
2020-01-08 17:30:53 +01:00
( Array . isArray ( result ? . errors ) && result . errors . length > 0 )
2019-09-05 01:56:11 +02:00
}
2019-10-04 17:26:44 +02:00
renderError = renderError || ! ! result . error
2019-09-24 10:50:04 +02:00
if (
options . buildExport &&
typeof result . fromBuildExportRevalidate !== 'undefined'
) {
configuration . initialPageRevalidationMap [ path ] =
result . fromBuildExportRevalidate
}
2019-09-05 01:56:11 +02:00
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-09-24 10:50:04 +02:00
// copy prerendered routes to outDir
if ( ! options . buildExport && prerenderManifest ) {
await Promise . all (
Object . keys ( prerenderManifest . routes ) . map ( async route = > {
2020-02-05 22:10:39 +01:00
route = normalizePagePath ( route )
2019-09-24 10:50:04 +02:00
const orig = join ( distPagesDir , route )
2019-10-31 21:26:47 +01:00
const htmlDest = join (
outDir ,
` ${ route } ${
subFolders && route !== '/index' ? ` ${ sep } index ` : ''
} . html `
)
2020-03-25 09:22:34 +01:00
const ampHtmlDest = join (
outDir ,
` ${ route } .amp ${ subFolders ? ` ${ sep } index ` : '' } .html `
)
2020-01-15 02:22:15 +01:00
const jsonDest = join ( pagesDataDir , ` ${ route } .json ` )
2019-09-24 10:50:04 +02:00
2020-05-02 06:10:19 +02:00
await promises . mkdir ( dirname ( htmlDest ) , { recursive : true } )
await promises . mkdir ( dirname ( jsonDest ) , { recursive : true } )
await promises . copyFile ( ` ${ orig } .html ` , htmlDest )
await promises . copyFile ( ` ${ orig } .json ` , jsonDest )
2020-03-25 09:22:34 +01:00
if ( await exists ( ` ${ orig } .amp.html ` ) ) {
2020-05-02 06:10:19 +02:00
await promises . mkdir ( dirname ( ampHtmlDest ) , { recursive : true } )
await promises . copyFile ( ` ${ orig } .amp.html ` , ampHtmlDest )
2020-03-25 09:22:34 +01:00
}
2019-09-24 10:50:04 +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 ( '' )
2019-12-03 17:18:58 +01:00
2019-12-13 20:30:22 +01:00
writeFileSync (
join ( distDir , EXPORT_DETAIL ) ,
JSON . stringify ( {
version : 1 ,
outDirectory : outDir ,
success : true ,
} ) ,
'utf8'
)
2019-12-03 17:18:58 +01:00
if ( telemetry ) {
await telemetry . flush ( )
}
2017-05-08 00:47:40 +02:00
}