2020-11-06 03:33:14 +01:00
import chalk from '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'
2020-08-04 09:58:23 +02:00
import * as Log from '../build/output/log'
2019-10-04 17:26:44 +02:00
import createSpinner from '../build/spinner'
2020-05-19 15:29:34 +02:00
import { API_ROUTE , SSG_FALLBACK_EXPORT_ERROR } from '../lib/constants'
2019-09-05 01:56:11 +02:00
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 ,
2020-11-11 16:46:48 +01:00
EXPORT_MARKER ,
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 ,
2020-12-04 11:14:55 +01:00
NextConfig ,
2019-10-04 17:26:44 +02:00
} from '../next-server/server/config'
2020-02-14 21:42:44 +01:00
import { eventCliSession } from '../telemetry/events'
2020-10-05 17:26:11 +02:00
import { hasNextSupport } from '../telemetry/ci-info'
2019-10-10 19:18:07 +02:00
import { Telemetry } from '../telemetry/storage'
2020-06-04 19:32:45 +02:00
import {
normalizePagePath ,
denormalizePagePath ,
} from '../next-server/server/normalize-page-path'
2020-09-25 20:14:28 +02:00
import { loadEnvConfig } from '@next/env'
2020-05-19 15:29:34 +02:00
import { PrerenderManifest } from '../build'
2020-12-28 23:04:51 +01:00
import exportPage from './worker'
2020-05-25 23:15:56 +02:00
import { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
2020-06-22 23:12:36 +02:00
import { getPagePath } from '../next-server/server/require'
2021-01-10 02:12:13 +01:00
import { tracer , traceFn , traceAsyncFn } from '../build/tracer'
import opentelemetryApi from '@opentelemetry/api'
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
2020-09-07 11:52:12 +02:00
function divideSegments ( number : number , segments : number ) : number [ ] {
const result = [ ]
while ( number > 0 && segments > 0 ) {
2020-11-20 17:57:34 +01:00
const dividedNumber =
number < segments ? number : Math . floor ( number / segments )
2020-09-07 11:52:12 +02:00
number -= dividedNumber
segments --
result . push ( dividedNumber )
}
return result
}
2020-09-21 18:09:14 +02:00
const createProgress = ( total : number , label : string ) = > {
2020-09-07 11:52:12 +02:00
const segments = divideSegments ( total , 4 )
let currentSegmentTotal = segments . shift ( )
let currentSegmentCount = 0
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 ++
2020-09-07 11:52:12 +02:00
currentSegmentCount ++
// Make sure we only log once per fully generated segment
if ( currentSegmentCount !== currentSegmentTotal ) {
return
}
currentSegmentTotal = segments . shift ( )
currentSegmentCount = 0
2019-09-16 17:37:00 +02:00
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 } }
}
2020-05-25 23:15:56 +02:00
interface ExportOptions {
outdir : string
silent? : boolean
threads? : number
pages? : string [ ]
buildExport? : boolean
2020-08-04 09:58:23 +02:00
statusMessage? : string
2020-05-25 23:15:56 +02:00
}
export default async function exportApp (
2019-10-04 17:26:44 +02:00
dir : string ,
2020-05-25 23:15:56 +02:00
options : ExportOptions ,
2020-12-04 11:14:55 +01:00
configuration? : NextConfig
2019-10-04 17:26:44 +02:00
) : Promise < void > {
2021-01-10 02:12:13 +01:00
const nextExportSpan = tracer . startSpan ( 'next-export' )
return traceAsyncFn ( nextExportSpan , async ( ) = > {
dir = resolve ( dir )
2020-05-21 14:06:57 +02:00
2021-01-10 02:12:13 +01:00
// attempt to load global env values so they are available in next.config.js
traceFn ( tracer . startSpan ( 'load-dotenv' ) , ( ) = >
loadEnvConfig ( dir , false , Log )
2019-11-08 18:03:50 +01:00
)
2019-10-03 16:21:15 +02:00
2021-01-10 02:12:13 +01:00
const nextConfig =
configuration ||
traceFn ( tracer . startSpan ( 'load-next-config' ) , ( ) = >
loadConfig ( PHASE_EXPORT , dir )
)
const threads = options . threads || Math . max ( cpus ( ) . length - 1 , 1 )
const distDir = join ( dir , nextConfig . distDir )
const telemetry = options . buildExport ? null : new Telemetry ( { distDir } )
if ( telemetry ) {
telemetry . record (
eventCliSession ( PHASE_EXPORT , distDir , {
cliCommand : 'export' ,
isSrcDir : null ,
hasNowJson : ! ! ( await findUp ( 'now.json' , { cwd : dir } ) ) ,
isCustomServer : null ,
} )
)
2020-03-24 18:38:22 +01:00
}
2021-01-10 02:12:13 +01:00
const subFolders = nextConfig . trailingSlash
const isLikeServerless = nextConfig . target !== 'server'
2018-08-27 12:28:54 +02:00
2021-01-10 02:12:13 +01:00
if ( ! options . silent && ! options . buildExport ) {
Log . info ( ` using build directory: ${ distDir } ` )
2019-11-22 02:20:19 +01:00
}
2021-01-10 02:12:13 +01:00
const buildIdFile = join ( distDir , BUILD_ID_FILE )
2019-10-06 13:44:03 +02:00
2021-01-10 02:12:13 +01:00
if ( ! existsSync ( buildIdFile ) ) {
throw new Error (
` Could not find a production build in the ' ${ distDir } ' directory. Try building your app with 'next build' before starting the static export. https://err.sh/vercel/next.js/next-export-no-build-id `
)
2020-08-04 09:58:23 +02:00
}
2017-05-14 02:11:13 +02:00
2021-01-10 02:12:13 +01:00
const customRoutesDetected = [ 'rewrites' , 'redirects' , 'headers' ] . filter (
( config ) = > typeof nextConfig [ config ] === 'function'
2018-02-07 11:54:07 +01:00
)
2021-01-10 02:12:13 +01:00
if (
! hasNextSupport &&
! options . buildExport &&
customRoutesDetected . length > 0
) {
Log . warn (
` rewrites, redirects, and headers are not applied when exporting your application, detected ( ${ customRoutesDetected . join (
', '
) } ) . See more info here : https : //err.sh/next.js/export-no-custom-routes`
2020-08-04 09:58:23 +02:00
)
}
2017-05-09 03:20:50 +02:00
2021-01-10 02:12:13 +01:00
const buildId = readFileSync ( buildIdFile , 'utf8' )
const pagesManifest =
! options . pages &&
( require ( join (
distDir ,
isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY ,
PAGES_MANIFEST
) ) as PagesManifest )
let prerenderManifest : PrerenderManifest | undefined = undefined
try {
prerenderManifest = require ( join ( distDir , PRERENDER_MANIFEST ) )
} catch ( _ ) { }
const excludedPrerenderRoutes = new Set < string > ( )
const pages = options . pages || Object . keys ( pagesManifest )
const defaultPathMap : ExportPathMap = { }
let hasApiRoutes = false
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 . match ( API_ROUTE ) ) {
hasApiRoutes = true
continue
}
2020-10-15 20:21:30 +02:00
2021-01-10 02:12:13 +01:00
if ( page === '/_document' || page === '/_app' || page === '/_error' ) {
continue
}
2020-11-11 16:46:48 +01:00
2021-01-10 02:12:13 +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`.
if ( prerenderManifest ? . dynamicRoutes [ page ] ) {
excludedPrerenderRoutes . add ( page )
continue
}
2018-02-26 12:03:27 +01:00
2021-01-10 02:12:13 +01:00
defaultPathMap [ page ] = { page }
}
2018-02-27 17:50:14 +01:00
2021-01-10 02:12:13 +01:00
// Initialize the output directory
const outDir = options . outdir
2017-05-08 08:10:26 +02:00
2021-01-10 02:12:13 +01: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/vercel/next.js/can-not-output-to-public `
)
}
2017-05-08 08:10:26 +02:00
2021-01-11 14:34:58 +01:00
if ( outDir === join ( dir , 'static' ) ) {
throw new Error (
` The 'static' directory is reserved in Next.js and can not be used as the export out directory. https://err.sh/vercel/next.js/can-not-output-to-static `
)
}
2021-01-10 02:12:13 +01:00
await recursiveDelete ( join ( outDir ) )
await promises . mkdir ( join ( outDir , '_next' , buildId ) , { recursive : true } )
writeFileSync (
join ( distDir , EXPORT_DETAIL ) ,
JSON . stringify ( {
version : 1 ,
outDirectory : outDir ,
success : false ,
} ) ,
'utf8'
)
2020-05-27 07:48:50 +02:00
2021-01-10 02:12:13 +01:00
// Copy static directory
if ( ! options . buildExport && existsSync ( join ( dir , 'static' ) ) ) {
if ( ! options . silent ) {
Log . info ( 'Copying "static" directory' )
}
await traceAsyncFn ( tracer . startSpan ( 'copy-static-directory' ) , ( ) = >
recursiveCopy ( join ( dir , 'static' ) , join ( outDir , 'static' ) )
)
2019-09-04 16:00:54 +02:00
}
2020-05-27 07:48:50 +02:00
2021-01-10 02:12:13 +01:00
// Copy .next/static directory
if (
! options . buildExport &&
existsSync ( join ( distDir , CLIENT_STATIC_FILES_PATH ) )
) {
if ( ! options . silent ) {
Log . info ( 'Copying "static build" directory' )
}
await traceAsyncFn ( tracer . startSpan ( 'copy-next-static-directory' ) , ( ) = >
recursiveCopy (
join ( distDir , CLIENT_STATIC_FILES_PATH ) ,
join ( outDir , '_next' , CLIENT_STATIC_FILES_PATH )
)
2020-05-27 07:48:50 +02:00
)
2021-01-10 02:12:13 +01:00
}
2020-05-19 15:29:34 +02:00
2021-01-10 02:12:13 +01:00
// Get the exportPathMap from the config file
if ( typeof nextConfig . exportPathMap !== 'function' ) {
if ( ! options . silent ) {
Log . info (
` No "exportPathMap" found in " ${ CONFIG_FILE } ". Generating map from "./pages" `
)
2020-05-19 15:29:34 +02:00
}
2021-01-10 02:12:13 +01:00
nextConfig . exportPathMap = async ( defaultMap : ExportPathMap ) = > {
return defaultMap
2020-05-19 15:29:34 +02:00
}
}
2021-01-10 02:12:13 +01:00
const {
i18n ,
images : { loader = 'default' } ,
} = nextConfig
if ( i18n && ! options . buildExport ) {
2020-05-19 15:29:34 +02:00
throw new Error (
2021-01-10 02:12:13 +01:00
` i18n support is not compatible with next export. See here for more info on deploying: https://nextjs.org/docs/deployment `
2020-05-19 15:29:34 +02:00
)
}
2021-01-10 02:12:13 +01:00
if ( ! options . buildExport ) {
const { isNextImageImported } = await traceAsyncFn (
tracer . startSpan ( 'is-next-image-imported' ) ,
( ) = >
promises
. readFile ( join ( distDir , EXPORT_MARKER ) , 'utf8' )
. then ( ( text ) = > JSON . parse ( text ) )
. catch ( ( ) = > ( { } ) )
2020-08-04 09:58:23 +02:00
)
2021-01-10 02:12:13 +01:00
if ( isNextImageImported && loader === 'default' && ! hasNextSupport ) {
throw new Error (
` Image Optimization using Next.js' default loader is not compatible with \` next export \` .
Possible solutions :
- Use \ ` next start \` , which starts the Image Optimization API.
- Use Vercel to deploy , which supports Image Optimization .
- Configure a third - party loader in \ ` next.config.js \` .
Read more : https : //err.sh/next.js/export-image-api`
)
}
2020-08-04 09:58:23 +02:00
}
2018-05-11 14:52:39 +02:00
2021-01-10 02:12:13 +01:00
// Start the rendering process
const renderOpts = {
dir ,
buildId ,
nextExport : true ,
assetPrefix : nextConfig.assetPrefix.replace ( /\/$/ , '' ) ,
distDir ,
dev : false ,
hotReloader : null ,
basePath : nextConfig.basePath ,
canonicalBase : nextConfig.amp?.canonicalBase || '' ,
ampValidatorPath : nextConfig.experimental.amp?.validator || undefined ,
ampSkipValidation : nextConfig.experimental.amp?.skipValidation || false ,
ampOptimizerConfig : nextConfig.experimental.amp?.optimizer || undefined ,
locales : i18n?.locales ,
locale : i18n?.defaultLocale ,
defaultLocale : i18n?.defaultLocale ,
domainLocales : i18n?.domains ,
2020-08-04 09:58:23 +02:00
}
2019-09-05 01:56:11 +02:00
2021-01-10 02:12:13 +01:00
const { serverRuntimeConfig , publicRuntimeConfig } = nextConfig
2019-09-05 01:56:11 +02:00
2021-01-10 02:12:13 +01:00
if ( Object . keys ( publicRuntimeConfig ) . length > 0 ) {
; ( renderOpts as any ) . runtimeConfig = publicRuntimeConfig
}
2019-09-05 01:56:11 +02:00
2021-01-10 02:12:13 +01:00
// We need this for server rendering the Link component.
; ( global as any ) . __NEXT_DATA__ = {
nextExport : true ,
}
2019-05-03 18:57:47 +02:00
2021-01-10 02:12:13 +01:00
if ( ! options . silent && ! options . buildExport ) {
Log . info ( ` Launching ${ threads } workers ` )
}
const exportPathMap = await traceAsyncFn (
tracer . startSpan ( 'run-export-path-map' ) ,
( ) = >
nextConfig . exportPathMap ( defaultPathMap , {
dev : false ,
dir ,
outDir ,
distDir ,
buildId ,
} )
)
2019-09-05 01:56:11 +02:00
2021-01-10 02:12:13 +01:00
if ( ! exportPathMap [ '/404' ] && ! exportPathMap [ '/404.html' ] ) {
exportPathMap [ '/404' ] = exportPathMap [ '/404.html' ] = {
page : '/_error' ,
2019-09-05 01:56:11 +02:00
}
2021-01-10 02:12:13 +01:00
}
// make sure to prevent duplicates
const exportPaths = [
. . . new Set (
Object . keys ( exportPathMap ) . map ( ( path ) = >
denormalizePagePath ( normalizePagePath ( path ) )
)
) ,
]
const filteredPaths = exportPaths . filter (
// Remove API routes
( route ) = > ! exportPathMap [ route ] . page . match ( API_ROUTE )
)
if ( filteredPaths . length !== exportPaths . length ) {
hasApiRoutes = true
}
if ( prerenderManifest && ! options . buildExport ) {
const fallbackEnabledPages = new Set ( )
2019-09-24 10:50:04 +02:00
2021-01-10 02:12:13 +01:00
for ( const key of Object . keys ( prerenderManifest . dynamicRoutes ) ) {
// only error if page is included in path map
if ( ! exportPathMap [ key ] && ! excludedPrerenderRoutes . has ( key ) ) {
continue
2020-10-15 23:55:38 +02:00
}
2021-01-10 02:12:13 +01:00
if ( prerenderManifest . dynamicRoutes [ key ] . fallback !== false ) {
fallbackEnabledPages . add ( key )
2020-10-15 23:55:38 +02:00
}
2019-09-24 10:50:04 +02:00
}
2020-10-15 23:55:38 +02:00
2021-01-10 02:12:13 +01:00
if ( fallbackEnabledPages . size ) {
throw new Error (
` Found pages with \` fallback \` enabled: \ n ${ [
. . . fallbackEnabledPages ,
] . join ( '\n' ) } \ n $ { SSG_FALLBACK_EXPORT_ERROR } \ n `
2020-06-22 23:12:36 +02:00
)
2021-01-10 02:12:13 +01:00
}
}
2020-06-22 23:12:36 +02:00
2021-01-10 02:12:13 +01:00
// Warn if the user defines a path for an API page
if ( hasApiRoutes ) {
if ( ! options . silent ) {
Log . warn (
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 \` . `
) +
` \ n ` +
chalk . yellow (
` Learn more: https://err.sh/vercel/next.js/api-routes-static-export `
)
2020-03-25 09:22:34 +01:00
)
2021-01-10 02:12:13 +01:00
}
}
2019-09-24 10:50:04 +02:00
2021-01-10 02:12:13 +01:00
const progress =
! options . silent &&
createProgress (
filteredPaths . length ,
` ${ Log . prefixes . info } ${ options . statusMessage || 'Exporting' } `
)
const pagesDataDir = options . buildExport
? outDir
: join ( outDir , '_next/data' , buildId )
const ampValidations : AmpPageStatus = { }
let hadValidationError = false
const publicDir = join ( dir , CLIENT_PUBLIC_FILES_PATH )
// Copy public directory
if ( ! options . buildExport && existsSync ( publicDir ) ) {
if ( ! options . silent ) {
Log . info ( 'Copying "public" directory' )
}
await traceAsyncFn ( tracer . startSpan ( 'copy-public-directory' ) , ( ) = >
recursiveCopy ( publicDir , outDir , {
filter ( path ) {
// Exclude paths used by pages
return ! exportPathMap [ path ]
} ,
} )
)
}
2020-03-25 09:22:34 +01:00
2021-01-10 02:12:13 +01:00
const worker = new Worker ( require . resolve ( './worker' ) , {
maxRetries : 0 ,
numWorkers : threads ,
enableWorkerThreads : true ,
exposedMethods : [ 'default' ] ,
} ) as Worker & { default : typeof exportPage }
worker . getStdout ( ) . pipe ( process . stdout )
worker . getStderr ( ) . pipe ( process . stderr )
let renderError = false
const errorPaths : string [ ] = [ ]
await Promise . all (
filteredPaths . map ( async ( path ) = > {
const pageExportSpan = tracer . startSpan ( 'export-page' , {
attributes : { path } ,
} )
return traceAsyncFn ( pageExportSpan , async ( ) = > {
const spanContext = { }
opentelemetryApi . propagation . inject (
opentelemetryApi . context . active ( ) ,
spanContext
)
const result = await worker . default ( {
path ,
pathMap : exportPathMap [ path ] ,
distDir ,
outDir ,
pagesDataDir ,
renderOpts ,
serverRuntimeConfig ,
subFolders ,
buildExport : options.buildExport ,
serverless : isTargetLikeServerless ( nextConfig . target ) ,
optimizeFonts : nextConfig.experimental.optimizeFonts ,
optimizeImages : nextConfig.experimental.optimizeImages ,
optimizeCss : nextConfig.experimental.optimizeCss ,
spanContext ,
} )
for ( const validation of result . ampValidations || [ ] ) {
const { page , result : ampValidationResult } = validation
ampValidations [ page ] = ampValidationResult
hadValidationError =
hadValidationError ||
( Array . isArray ( ampValidationResult ? . errors ) &&
ampValidationResult . errors . length > 0 )
}
renderError = renderError || ! ! result . error
if ( ! ! result . error ) errorPaths . push ( path )
if ( options . buildExport && configuration ) {
if ( typeof result . fromBuildExportRevalidate !== 'undefined' ) {
configuration . initialPageRevalidationMap [ path ] =
result . fromBuildExportRevalidate
}
if ( result . ssgNotFound === true ) {
configuration . ssgNotFoundPaths . push ( path )
}
}
if ( progress ) progress ( )
} )
2019-09-24 10:50:04 +02:00
} )
)
2021-01-10 02:12:13 +01:00
worker . end ( )
// copy prerendered routes to outDir
if ( ! options . buildExport && prerenderManifest ) {
await Promise . all (
Object . keys ( prerenderManifest . routes ) . map ( async ( route ) = > {
const { srcRoute } = prerenderManifest ! . routes [ route ]
const pageName = srcRoute || route
const pagePath = getPagePath ( pageName , distDir , isLikeServerless )
const distPagesDir = join (
pagePath ,
// strip leading / and then recurse number of nested dirs
// to place from base folder
pageName
. substr ( 1 )
. split ( '/' )
. map ( ( ) = > '..' )
. join ( '/' )
)
route = normalizePagePath ( route )
const orig = join ( distPagesDir , route )
const htmlDest = join (
outDir ,
` ${ route } ${
subFolders && route !== '/index' ? ` ${ sep } index ` : ''
} . html `
)
const ampHtmlDest = join (
outDir ,
` ${ route } .amp ${ subFolders ? ` ${ sep } index ` : '' } .html `
)
const jsonDest = join ( pagesDataDir , ` ${ route } .json ` )
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 )
if ( await exists ( ` ${ orig } .amp.html ` ) ) {
await promises . mkdir ( dirname ( ampHtmlDest ) , { recursive : true } )
await promises . copyFile ( ` ${ orig } .amp.html ` , ampHtmlDest )
}
} )
)
}
if ( Object . keys ( ampValidations ) . length ) {
console . log ( formatAmpMessages ( ampValidations ) )
}
if ( hadValidationError ) {
throw new Error (
` AMP Validation caused the export to fail. https://err.sh/vercel/next.js/amp-export-validation `
)
}
if ( renderError ) {
throw new Error (
` Export encountered errors on following paths: \ n \ t ${ errorPaths
. sort ( )
. join ( '\n\t' ) } `
)
}
2019-03-26 22:21:27 +01:00
2021-01-10 02:12:13 +01:00
writeFileSync (
join ( distDir , EXPORT_DETAIL ) ,
JSON . stringify ( {
version : 1 ,
outDirectory : outDir ,
success : true ,
} ) ,
'utf8'
Add failing paths to export error summary (#10026)
Closes #9990 by collecting all paths with errors during `next export` and reporting them sorted in the error summary at the end.
It will produce an output similar to:
```
Error: Export encountered errors on following paths:
/nested/page
/page
/page-1
/page-10
/page-11
/page-12
/page-13
/page-2
/page-3
/page-4
/page-5
/page-6
/page-7
/page-8
/page-9
at _default (/app/next.js/packages/next/dist/export/index.js:19:788)
at process._tickCallback (internal/process/next_tick.js:68:7)
```
I tested the output with the `handle-export-errors` integration test suite, but I'm not sure how to gracefully test this added output.
I thought of collecting all page source files with [recursiveReaddirSync](https://github.com/zeit/next.js/blob/2ba352da39ee00b6595aecdc9ffb2f103e803a85/packages/next/next-server/server/lib/recursive-readdir-sync.ts) but it seems I can't import it in js test files:
```
SyntaxError: /app/next.js/packages/next/next-server/server/lib/recursive-readdir-sync.ts: Unexpected token, expected "," (11:5)
9 | */
10 | export function recursiveReadDirSync(
> 11 | dir: string,
| ^
12 | arr: string[] = [],
13 | rootDir = dir
14 | ): string[] {
```
The test itself could look like:
```js
it('Reports failing paths', async () => {
const { stderr } = await nextBuild(appDir, [], {
stdout: true,
stderr: true,
})
const pages = []
// collect pages to be ['/page', '/page-1', ... etc.]
pages.forEach(page => {
expect(stderr).toContain(page)
})
})
```
2020-05-26 21:50:25 +02:00
)
2019-12-03 17:18:58 +01:00
2021-01-10 02:12:13 +01:00
if ( telemetry ) {
await telemetry . flush ( )
}
} )
2017-05-08 00:47:40 +02:00
}