2018-12-12 13:59:11 +01:00
import { cpus } from 'os'
import { fork } from 'child_process'
2017-05-08 00:47:40 +02:00
import cp from 'recursive-copy'
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'
import { existsSync , readFileSync } from 'fs'
2018-10-02 00:55:31 +02:00
import loadConfig from 'next-server/next-config'
2019-05-03 18:57:47 +02:00
import { PHASE _EXPORT , SERVER _DIRECTORY , PAGES _MANIFEST , CONFIG _FILE , BUILD _ID _FILE , CLIENT _PUBLIC _FILES _PATH , CLIENT _STATIC _FILES _PATH } from 'next-server/constants'
2018-12-12 13:59:11 +01:00
import createProgress from 'tty-aware-progress'
2019-02-22 17:33:28 +01:00
import { promisify } from 'util'
2019-03-05 14:01:42 +01:00
import { recursiveDelete } from '../lib/recursive-delete'
2019-03-26 22:21:27 +01:00
import { formatAmpMessages } from '../build/output/index'
2019-02-22 17:33:28 +01:00
const mkdirp = promisify ( mkdirpModule )
2017-05-08 00:47:40 +02:00
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 concurrency = options . concurrency || 10
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-04-22 18:55:03 +02:00
const subFolders = nextConfig . experimental . exportTrailingSlash
2017-05-08 00:47:40 +02:00
2019-05-22 18:36:53 +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 ) ) {
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-22 18:36:53 +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
if ( page === '/_document' || page === '/_app' || page === '/_error' ) {
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' )
await cp (
join ( dir , 'static' ) ,
2018-01-31 08:38:43 +01:00
join ( outDir , 'static' ) ,
{ expand : true }
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' )
await cp (
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' ) {
2018-06-04 11:38:46 +02:00
console . log ( ` > No "exportPathMap" found in " ${ CONFIG _FILE } ". Generating map from "./pages" ` )
2018-03-30 15:08:09 +02:00
nextConfig . exportPathMap = async ( defaultMap ) => {
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 ,
canonicalBase : ( nextConfig . amp && nextConfig . amp . canonicalBase ) || ''
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
if ( publicRuntimeConfig ) {
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
}
2018-12-12 13:59:11 +01:00
log ( ` launching ${ threads } threads with concurrency of ${ concurrency } per thread ` )
2019-02-19 22:45:07 +01:00
const exportPathMap = await nextConfig . exportPathMap ( defaultPathMap , { dev : false , dir , outDir , distDir , buildId } )
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
exportPathMap [ '/404.html' ] = exportPathMap [ '/404.html' ] || { page : '/_error' }
2018-05-11 14:52:39 +02:00
const exportPaths = Object . keys ( exportPathMap )
2018-12-12 13:59:11 +01:00
const progress = ! options . silent && createProgress ( exportPaths . length )
2017-05-09 03:53:08 +02:00
2018-12-12 13:59:11 +01:00
const chunks = exportPaths . reduce ( ( result , route , i ) => {
const worker = i % threads
if ( ! result [ worker ] ) {
result [ worker ] = { paths : [ ] , pathMap : { } }
2017-10-05 20:33:10 +02:00
}
2018-12-12 13:59:11 +01:00
result [ worker ] . pathMap [ route ] = exportPathMap [ route ]
result [ worker ] . paths . push ( route )
return result
} , [ ] )
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
if ( existsSync ( publicDir ) ) {
log ( ' copying "public" directory' )
await cp (
publicDir ,
outDir ,
{
expand : true ,
filter ( path ) {
// Exclude paths used by pages
return ! exportPathMap [ '/' + path ]
}
}
)
}
2019-05-22 18:36:53 +02:00
const workers = new Set ( )
2019-05-03 18:57:47 +02:00
2018-12-12 13:59:11 +01:00
await Promise . all (
chunks . map (
chunk =>
new Promise ( ( resolve , reject ) => {
const worker = fork ( require . resolve ( './worker' ) , [ ] , {
env : process . env
} )
2019-05-22 18:36:53 +02:00
workers . add ( worker )
2018-12-12 13:59:11 +01:00
worker . send ( {
2018-12-18 17:12:49 +01:00
distDir ,
buildId ,
2018-12-12 13:59:11 +01:00
exportPaths : chunk . paths ,
exportPathMap : chunk . pathMap ,
outDir ,
renderOpts ,
2019-02-12 02:28:47 +01:00
serverRuntimeConfig ,
2019-04-22 18:55:03 +02:00
concurrency ,
2019-05-22 18:36:53 +02:00
subFolders ,
serverless : nextConfig . target === 'serverless'
2018-12-12 13:59:11 +01:00
} )
worker . on ( 'message' , ( { type , payload } ) => {
if ( type === 'progress' && progress ) {
progress ( )
} else if ( type === 'error' ) {
reject ( payload )
} else if ( type === 'done' ) {
resolve ( )
2019-03-26 22:21:27 +01:00
} else if ( type === 'amp-validation' ) {
ampValidations [ payload . page ] = payload . result
hadValidationError = hadValidationError || payload . result . errors . length
2018-12-12 13:59:11 +01:00
}
} )
} )
)
)
2017-05-11 18:23:08 +02:00
2019-05-22 18:36:53 +02:00
workers . forEach ( worker => worker . kill ( ) )
2019-03-26 22:21:27 +01:00
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 ` )
}
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
}