2019-04-10 18:37:13 +02:00
import chalk from 'chalk'
2019-04-24 11:04:36 +02:00
import {
2019-05-22 18:36:53 +02:00
SERVER_DIRECTORY ,
SERVERLESS_DIRECTORY ,
PAGES_MANIFEST ,
2019-04-24 11:04:36 +02:00
CHUNK_GRAPH_MANIFEST ,
PHASE_PRODUCTION_BUILD ,
} from 'next-server/constants'
2019-04-10 18:37:13 +02:00
import loadConfig from 'next-server/next-config'
import nanoid from 'next/dist/compiled/nanoid/index.js'
import path from 'path'
2019-05-22 18:36:53 +02:00
import fs from 'fs'
import { promisify } from 'util'
2019-06-05 20:15:42 +02:00
import formatWebpackMessages from '../client/dev/error-overlay/format-webpack-messages'
2019-04-10 18:41:59 +02:00
import { recursiveDelete } from '../lib/recursive-delete'
2019-05-09 04:51:23 +02:00
import { verifyTypeScriptSetup } from '../lib/verifyTypeScriptSetup'
2019-04-10 18:37:13 +02:00
import { CompilerResult , runCompiler } from './compiler'
import { createEntrypoints , createPagesMapping } from './entries'
2019-04-10 18:41:59 +02:00
import { FlyingShuttle } from './flying-shuttle'
2019-02-17 12:56:48 +01:00
import { generateBuildId } from './generate-build-id'
import { isWriteable } from './is-writeable'
2019-04-10 18:41:59 +02:00
import {
collectPages ,
2019-04-10 21:19:50 +02:00
getCacheIdentifier ,
2019-04-10 18:41:59 +02:00
getFileForPage ,
2019-05-14 17:11:22 +02:00
getPageSizeInKb ,
2019-04-10 18:41:59 +02:00
getSpecifiedPages ,
printTreeView ,
2019-05-14 17:11:22 +02:00
PageInfo ,
2019-05-22 18:36:53 +02:00
isPageStatic ,
hasCustomAppGetInitialProps ,
2019-04-10 18:41:59 +02:00
} from './utils'
2019-04-10 18:37:13 +02:00
import getBaseWebpackConfig from './webpack-config'
2019-05-09 04:51:23 +02:00
import {
exportManifest ,
getPageChunks ,
} from './webpack/plugins/chunk-graph-plugin'
2019-04-10 18:37:13 +02:00
import { writeBuildId } from './write-build-id'
2019-05-29 13:57:26 +02:00
import { recursiveReadDir } from '../lib/recursive-readdir'
2019-05-22 18:36:53 +02:00
import mkdirpOrig from 'mkdirp'
2019-06-26 04:54:28 +02:00
import workerFarm from 'worker-farm'
import { Sema } from 'async-sema'
2019-05-22 18:36:53 +02:00
const fsUnlink = promisify ( fs . unlink )
const fsRmdir = promisify ( fs . rmdir )
const fsMove = promisify ( fs . rename )
const fsReadFile = promisify ( fs . readFile )
const fsWriteFile = promisify ( fs . writeFile )
const mkdirp = promisify ( mkdirpOrig )
2019-04-06 19:11:38 +02:00
2019-06-26 04:54:28 +02:00
const staticCheckWorker = require . resolve ( './static-checker' )
2019-04-24 21:16:30 +02:00
export default async function build ( dir : string , conf = null ) : Promise < void > {
2019-02-17 12:56:48 +01:00
if ( ! ( await isWriteable ( dir ) ) ) {
throw new Error (
'> Build directory is not writeable. https://err.sh/zeit/next.js/build-dir-not-writeable'
)
2018-12-03 14:18:52 +01:00
}
2019-05-09 04:51:23 +02:00
await verifyTypeScriptSetup ( dir )
2019-07-01 19:13:06 +02:00
console . log ( 'Creating an optimized production build ...' )
2019-02-17 12:56:48 +01:00
console . log ( )
2018-12-03 14:18:52 +01:00
const config = loadConfig ( PHASE_PRODUCTION_BUILD , dir , conf )
2019-06-06 15:57:42 +02:00
const { target } = config
2019-07-01 19:13:06 +02:00
const buildId = await generateBuildId ( config . generateBuildId , nanoid )
2019-04-03 02:22:04 +02:00
const distDir = path . join ( dir , config . distDir )
const pagesDir = path . join ( dir , 'pages' )
2019-04-10 18:37:13 +02:00
const isFlyingShuttle = Boolean (
config . experimental . flyingShuttle &&
! process . env . __NEXT_BUILDER_EXPERIMENTAL_PAGE
)
2019-04-10 04:57:46 +02:00
const selectivePageBuilding = Boolean (
2019-04-10 18:37:13 +02:00
isFlyingShuttle || process . env . __NEXT_BUILDER_EXPERIMENTAL_PAGE
2019-04-10 04:57:46 +02:00
)
2019-04-04 23:54:01 +02:00
2019-04-24 20:32:15 +02:00
if ( selectivePageBuilding && target !== 'serverless' ) {
2019-04-10 05:15:35 +02:00
throw new Error (
` Cannot use ${
2019-04-10 18:37:13 +02:00
isFlyingShuttle ? 'flying shuttle' : '`now dev`'
2019-04-10 05:15:35 +02:00
} without the serverless target . `
)
2019-04-05 02:19:58 +02:00
}
2019-04-03 02:22:04 +02:00
2019-04-10 21:19:50 +02:00
const selectivePageBuildingCacheIdentifier = selectivePageBuilding
? await getCacheIdentifier ( {
pagesDirectory : pagesDir ,
env : config.env || { } ,
} )
: 'noop'
2019-04-10 18:41:59 +02:00
let flyingShuttle : FlyingShuttle | undefined
if ( isFlyingShuttle ) {
console . log ( chalk . magenta ( 'Building with Flying Shuttle enabled ...' ) )
console . log ( )
2019-04-11 23:09:12 +02:00
await recursiveDelete ( distDir , /^(?!cache(?:[\/\\]|$)).*$/ )
2019-04-26 15:08:38 +02:00
await recursiveDelete ( path . join ( distDir , 'cache' , 'next-minifier' ) )
2019-05-17 21:39:18 +02:00
await recursiveDelete ( path . join ( distDir , 'cache' , 'next-babel-loader' ) )
2019-04-10 18:41:59 +02:00
flyingShuttle = new FlyingShuttle ( {
buildId ,
pagesDirectory : pagesDir ,
distDirectory : distDir ,
2019-04-10 21:19:50 +02:00
cacheIdentifier : selectivePageBuildingCacheIdentifier ,
2019-04-10 18:41:59 +02:00
} )
}
let pagePaths : string [ ]
2019-04-10 04:57:46 +02:00
if ( process . env . __NEXT_BUILDER_EXPERIMENTAL_PAGE ) {
2019-04-10 04:05:40 +02:00
pagePaths = await getSpecifiedPages (
dir ,
2019-04-10 04:57:46 +02:00
process . env . __NEXT_BUILDER_EXPERIMENTAL_PAGE ! ,
config . pageExtensions
2019-04-10 04:05:40 +02:00
)
2019-03-27 16:51:05 +01:00
} else {
pagePaths = await collectPages ( pagesDir , config . pageExtensions )
}
2019-05-22 18:36:53 +02:00
// needed for static exporting since we want to replace with HTML
// files even when flying shuttle doesn't rebuild the files
const allPagePaths = [ . . . pagePaths ]
const allStaticPages = new Set < string > ( )
let allPageInfos = new Map < string , PageInfo > ( )
2019-04-10 18:41:59 +02:00
if ( flyingShuttle && ( await flyingShuttle . hasShuttle ( ) ) ) {
2019-05-22 18:36:53 +02:00
allPageInfos = await flyingShuttle . getPageInfos ( )
2019-04-10 18:41:59 +02:00
const _unchangedPages = new Set ( await flyingShuttle . getUnchangedPages ( ) )
for ( const unchangedPage of _unchangedPages ) {
2019-05-29 13:57:26 +02:00
const info = allPageInfos . get ( unchangedPage ) || ( { } as PageInfo )
2019-05-22 18:36:53 +02:00
if ( info . static ) allStaticPages . add ( unchangedPage )
2019-05-29 13:57:26 +02:00
const recalled = await flyingShuttle . restorePage ( unchangedPage , info )
2019-04-10 18:41:59 +02:00
if ( recalled ) {
continue
}
_unchangedPages . delete ( unchangedPage )
}
2019-05-22 18:36:53 +02:00
const unchangedPages = ( await Promise . all (
2019-04-10 18:41:59 +02:00
[ . . . _unchangedPages ] . map ( async page = > {
2019-05-22 18:36:53 +02:00
if (
page . endsWith ( '.amp' ) &&
2019-05-29 13:57:26 +02:00
( allPageInfos . get ( page . split ( '.amp' ) [ 0 ] ) || ( { } as PageInfo ) ) . isAmp
2019-05-22 18:36:53 +02:00
) {
return ''
}
2019-04-10 18:41:59 +02:00
const file = await getFileForPage ( {
page ,
pagesDirectory : pagesDir ,
pageExtensions : config.pageExtensions ,
} )
if ( file ) {
return file
}
return Promise . reject (
new Error (
` Failed to locate page file: ${ page } . ` +
` Did pageExtensions change? We can't recover from this yet. `
)
)
2019-05-29 13:57:26 +02:00
} )
) ) . filter ( Boolean )
2019-04-10 18:41:59 +02:00
const pageSet = new Set ( pagePaths )
for ( const unchangedPage of unchangedPages ) {
pageSet . delete ( unchangedPage )
}
pagePaths = [ . . . pageSet ]
}
2019-05-22 18:36:53 +02:00
const allMappedPages = createPagesMapping ( allPagePaths , config . pageExtensions )
2019-03-27 16:51:05 +01:00
const mappedPages = createPagesMapping ( pagePaths , config . pageExtensions )
const entrypoints = createEntrypoints (
mappedPages ,
2019-04-24 20:32:15 +02:00
target ,
2019-03-27 16:51:05 +01:00
buildId ,
2019-04-10 04:57:46 +02:00
/* dynamicBuildId */ selectivePageBuilding ,
2019-03-27 16:51:05 +01:00
config
)
2019-03-10 15:46:50 +01:00
const configs = await Promise . all ( [
getBaseWebpackConfig ( dir , {
2019-02-17 12:56:48 +01:00
buildId ,
isServer : false ,
config ,
2019-04-24 20:32:15 +02:00
target ,
2019-02-17 12:56:48 +01:00
entrypoints : entrypoints.client ,
2019-04-10 04:05:40 +02:00
selectivePageBuilding ,
2019-02-17 12:56:48 +01:00
} ) ,
2019-03-10 15:46:50 +01:00
getBaseWebpackConfig ( dir , {
2019-02-17 12:56:48 +01:00
buildId ,
isServer : true ,
config ,
2019-04-24 20:32:15 +02:00
target ,
2019-02-17 12:56:48 +01:00
entrypoints : entrypoints.server ,
2019-04-10 04:05:40 +02:00
selectivePageBuilding ,
2019-02-17 12:56:48 +01:00
} ) ,
2019-03-10 15:46:50 +01:00
] )
2018-12-03 14:18:52 +01:00
2019-02-17 12:56:48 +01:00
let result : CompilerResult = { warnings : [ ] , errors : [ ] }
2019-04-24 20:32:15 +02:00
if ( target === 'serverless' ) {
2019-03-10 15:46:50 +01:00
const clientResult = await runCompiler ( configs [ 0 ] )
2019-01-10 22:10:50 +01:00
// Fail build if clientResult contains errors
2019-02-17 12:56:48 +01:00
if ( clientResult . errors . length > 0 ) {
result = {
warnings : [ . . . clientResult . warnings ] ,
errors : [ . . . clientResult . errors ] ,
}
2019-01-10 22:10:50 +01:00
} else {
2019-03-10 15:46:50 +01:00
const serverResult = await runCompiler ( configs [ 1 ] )
2019-02-17 12:56:48 +01:00
result = {
warnings : [ . . . clientResult . warnings , . . . serverResult . warnings ] ,
errors : [ . . . clientResult . errors , . . . serverResult . errors ] ,
}
2019-01-10 22:10:50 +01:00
}
2018-12-11 21:46:23 +01:00
} else {
result = await runCompiler ( configs )
}
2019-02-17 12:56:48 +01:00
result = formatWebpackMessages ( result )
2018-12-03 14:18:52 +01:00
2019-04-10 18:41:59 +02:00
if ( isFlyingShuttle ) {
console . log ( )
2019-04-24 11:04:36 +02:00
exportManifest ( {
dir : dir ,
fileName : path.join ( distDir , CHUNK_GRAPH_MANIFEST ) ,
selectivePageBuildingCacheIdentifier ,
} )
2019-04-10 18:41:59 +02:00
}
2018-12-03 14:18:52 +01:00
if ( result . errors . length > 0 ) {
2019-02-17 12:56:48 +01:00
// 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
}
2019-03-17 13:13:29 +01:00
const error = result . errors . join ( '\n\n' )
2019-02-17 12:56:48 +01:00
console . error ( chalk . red ( 'Failed to compile.\n' ) )
2019-07-15 17:16:35 +02:00
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/zeit/next.js/page-without-valid-component for more info. `
)
}
2019-03-17 13:13:29 +01:00
console . error ( error )
2019-02-17 12:56:48 +01:00
console . error ( )
2019-03-17 13:13:29 +01:00
if ( error . indexOf ( 'private-next-pages' ) > - 1 ) {
2019-03-27 16:51:05 +01:00
throw new Error (
'> webpack config.resolve.alias was incorrectly overriden. https://err.sh/zeit/next.js/invalid-resolve-alias'
)
2019-03-17 13:13:29 +01:00
}
2018-12-03 14:18:52 +01:00
throw new Error ( '> Build failed because of webpack errors' )
2019-02-17 12:56:48 +01:00
} else if ( result . warnings . length > 0 ) {
console . warn ( chalk . yellow ( 'Compiled with warnings.\n' ) )
console . warn ( result . warnings . join ( '\n\n' ) )
console . warn ( )
} else {
console . log ( chalk . green ( 'Compiled successfully.\n' ) )
2018-12-03 14:18:52 +01:00
}
2019-01-25 18:36:29 +01:00
2019-05-01 22:31:08 +02:00
const distPath = path . join ( dir , config . distDir )
2019-05-14 17:11:22 +02:00
const pageKeys = Object . keys ( mappedPages )
2019-05-29 13:57:26 +02:00
const manifestPath = path . join (
distDir ,
target === 'serverless' ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY ,
PAGES_MANIFEST
)
2019-05-22 18:36:53 +02:00
const staticPages = new Set < string > ( )
2019-06-14 02:08:19 +02:00
const invalidPages = new Set < string > ( )
2019-05-22 18:36:53 +02:00
const pageInfos = new Map < string , PageInfo > ( )
2019-06-28 22:01:11 +02:00
const pagesManifest = JSON . parse ( await fsReadFile ( manifestPath , 'utf8' ) )
2019-05-22 18:36:53 +02:00
let customAppGetInitialProps : boolean | undefined
process . env . NEXT_PHASE = PHASE_PRODUCTION_BUILD
2019-05-01 22:31:08 +02:00
2019-06-26 04:54:28 +02:00
const staticCheckSema = new Sema ( config . experimental . cpus , {
capacity : pageKeys.length ,
} )
const staticCheckWorkers = workerFarm (
{
2019-06-26 05:51:40 +02:00
maxConcurrentWorkers : config.experimental.cpus ,
2019-06-26 04:54:28 +02:00
} ,
staticCheckWorker ,
[ 'default' ]
)
2019-05-01 22:31:08 +02:00
2019-06-26 04:54:28 +02:00
await Promise . all (
pageKeys . map ( async page = > {
const chunks = getPageChunks ( page )
2019-05-22 18:36:53 +02:00
2019-06-26 04:54:28 +02:00
const actualPage = page === '/' ? '/index' : page
const size = await getPageSizeInKb ( actualPage , distPath , buildId )
const bundleRelative = path . join (
target === 'serverless' ? 'pages' : ` static/ ${ buildId } /pages ` ,
actualPage + '.js'
)
const serverBundle = path . join (
distPath ,
target === 'serverless' ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY ,
bundleRelative
)
2019-06-10 20:46:30 +02:00
2019-06-26 04:54:28 +02:00
let isStatic = false
2019-05-22 18:36:53 +02:00
2019-06-28 22:01:11 +02:00
pagesManifest [ page ] = bundleRelative . replace ( /\\/g , '/' )
2019-05-22 18:36:53 +02:00
2019-06-28 22:01:11 +02:00
const runtimeEnvConfig = {
publicRuntimeConfig : config.publicRuntimeConfig ,
serverRuntimeConfig : config.serverRuntimeConfig ,
}
const nonReservedPage = ! page . match ( /^\/(_app|_error|_document|api)/ )
if ( nonReservedPage && customAppGetInitialProps === undefined ) {
customAppGetInitialProps = hasCustomAppGetInitialProps (
target === 'serverless'
? serverBundle
: path . join (
distPath ,
SERVER_DIRECTORY ,
` /static/ ${ buildId } /pages/_app.js `
) ,
runtimeEnvConfig
)
if ( customAppGetInitialProps ) {
console . warn (
2019-07-05 17:57:16 +02:00
chalk . bold . yellow ( ` Warning: ` ) +
chalk . yellow (
` You have opted-out of Automatic Prerendering due to \` getInitialProps \` in \` pages/_app \` . `
)
)
console . warn (
'Read more: https://err.sh/next.js/opt-out-automatic-prerendering\n'
2019-05-29 13:57:26 +02:00
)
2019-06-28 22:01:11 +02:00
}
}
2019-06-26 04:54:28 +02:00
2019-07-01 23:13:52 +02:00
if ( nonReservedPage ) {
2019-06-28 22:01:11 +02:00
try {
await staticCheckSema . acquire ( )
const result : any = await new Promise ( ( resolve , reject ) = > {
staticCheckWorkers . default (
{ serverBundle , runtimeEnvConfig } ,
( error : Error | null , result : any ) = > {
if ( error ) return reject ( error )
resolve ( result || { } )
}
2019-06-26 04:54:28 +02:00
)
2019-06-28 22:01:11 +02:00
} )
staticCheckSema . release ( )
2019-05-22 18:36:53 +02:00
2019-07-01 23:13:52 +02:00
if (
( result . static && customAppGetInitialProps === false ) ||
result . prerender
) {
2019-06-28 22:01:11 +02:00
staticPages . add ( page )
isStatic = true
2019-06-14 02:08:19 +02:00
}
2019-06-28 22:01:11 +02:00
} catch ( err ) {
if ( err . message !== 'INVALID_DEFAULT_EXPORT' ) throw err
invalidPages . add ( page )
staticCheckSema . release ( )
2019-05-31 02:34:05 +02:00
}
2019-05-22 18:36:53 +02:00
}
2019-06-26 04:54:28 +02:00
pageInfos . set ( page , { size , chunks , serverBundle , static : isStatic } )
} )
)
workerFarm . end ( staticCheckWorkers )
2019-04-24 10:48:43 +02:00
2019-06-14 02:08:19 +02:00
if ( invalidPages . size > 0 ) {
throw new Error (
2019-07-15 17:16:35 +02:00
` automatic static optimization failed: found page ${
2019-06-14 02:08:19 +02:00
invalidPages . size === 1 ? '' : 's'
2019-07-15 17:16:35 +02:00
} without a React Component as default export in \ n $ { [ . . . invalidPages ]
2019-06-14 02:08:19 +02:00
. map ( pg = > ` pages ${ pg } ` )
. join (
'\n'
) } \ n \ nSee https : //err.sh/zeit/next.js/page-without-valid-component for more info.\n`
)
}
2019-04-24 10:48:43 +02:00
if ( Array . isArray ( configs [ 0 ] . plugins ) ) {
configs [ 0 ] . plugins . some ( ( plugin : any ) = > {
2019-05-14 17:11:22 +02:00
if ( ! plugin . ampPages ) {
return false
2019-05-01 22:31:08 +02:00
}
2019-05-14 17:11:22 +02:00
plugin . ampPages . forEach ( ( pg : any ) = > {
pageInfos . get ( pg ) ! . isAmp = true
} )
return true
2019-04-24 10:48:43 +02:00
} )
}
2019-05-22 18:36:53 +02:00
await writeBuildId ( distDir , buildId , selectivePageBuilding )
2019-06-28 22:01:11 +02:00
if ( staticPages . size > 0 ) {
2019-05-22 18:36:53 +02:00
const exportApp = require ( '../export' ) . default
const exportOptions = {
silent : true ,
buildExport : true ,
pages : Array.from ( staticPages ) ,
outdir : path.join ( distDir , 'export' ) ,
}
const exportConfig = {
. . . config ,
exportPathMap : ( defaultMap : any ) = > defaultMap ,
2019-07-03 19:25:44 +02:00
exportTrailingSlash : false ,
2019-05-22 18:36:53 +02:00
}
await exportApp ( dir , exportOptions , exportConfig )
const toMove = await recursiveReadDir ( exportOptions . outdir , /.*\.html$/ )
let serverDir : string = ''
// remove server bundles that were exported
for ( const page of staticPages ) {
const { serverBundle } = pageInfos . get ( page ) !
2019-06-05 07:17:36 +02:00
if ( ! serverDir ) {
2019-06-05 22:20:08 +02:00
serverDir = path . join (
serverBundle . split ( /(\/|\\)pages/ ) . shift ( ) ! ,
'pages'
)
2019-06-05 07:17:36 +02:00
}
2019-05-22 18:36:53 +02:00
await fsUnlink ( serverBundle )
}
for ( const file of toMove ) {
const orig = path . join ( exportOptions . outdir , file )
const dest = path . join ( serverDir , file )
const relativeDest = ( target === 'serverless'
? path . join ( 'pages' , file )
: path . join ( 'static' , buildId , 'pages' , file )
) . replace ( /\\/g , '/' )
let page = file . split ( '.html' ) [ 0 ] . replace ( /\\/g , '/' )
pagesManifest [ page ] = relativeDest
page = page === '/index' ? '/' : page
pagesManifest [ page ] = relativeDest
staticPages . add ( page )
await mkdirp ( path . dirname ( dest ) )
await fsMove ( orig , dest )
}
// remove temporary export folder
await recursiveDelete ( exportOptions . outdir )
await fsRmdir ( exportOptions . outdir )
await fsWriteFile ( manifestPath , JSON . stringify ( pagesManifest ) , 'utf8' )
}
staticPages . forEach ( pg = > allStaticPages . add ( pg ) )
pageInfos . forEach ( ( info : PageInfo , key : string ) = > {
allPageInfos . set ( key , info )
} )
2019-01-25 18:36:29 +01:00
2019-04-10 18:41:59 +02:00
if ( flyingShuttle ) {
2019-06-28 22:01:11 +02:00
await flyingShuttle . mergePagesManifest ( )
2019-05-22 18:36:53 +02:00
await flyingShuttle . save ( allStaticPages , pageInfos )
2019-04-10 18:41:59 +02:00
}
2019-06-24 20:59:51 +02:00
printTreeView (
Object . keys ( allMappedPages ) ,
allPageInfos ,
target === 'serverless'
)
2018-12-03 14:18:52 +01:00
}