2019-02-22 17:33:28 +01:00
import mkdirpModule from 'mkdirp'
import { promisify } from 'util'
2019-12-14 07:31:48 +01:00
import url from 'url'
2019-02-22 17:33:28 +01:00
import { extname , join , dirname , sep } from 'path'
2019-09-04 16:00:54 +02:00
import { renderToHTML } from '../next-server/server/render'
2019-08-02 16:28:03 +02:00
import { writeFile , access } from 'fs'
2019-03-26 22:21:27 +01:00
import AmpHtmlValidator from 'amphtml-validator'
2019-09-04 16:00:54 +02:00
import { loadComponents } from '../next-server/server/load-components'
import { isDynamicRoute } from '../next-server/lib/router/utils/is-dynamic'
import { getRouteMatcher } from '../next-server/lib/router/utils/route-matcher'
import { getRouteRegex } from '../next-server/lib/router/utils/route-regex'
2019-02-22 17:33:28 +01:00
2019-09-04 16:00:54 +02:00
const envConfig = require ( '../next-server/lib/runtime-config' )
2019-04-11 20:59:26 +02:00
const writeFileP = promisify ( writeFile )
2019-09-05 01:56:11 +02:00
const mkdirp = promisify ( mkdirpModule )
2019-04-11 20:59:26 +02:00
const accessP = promisify ( access )
2019-02-22 17:33:28 +01:00
2018-12-12 13:59:11 +01:00
global . _ _NEXT _DATA _ _ = {
2019-11-11 04:24:53 +01:00
nextExport : true ,
2018-12-12 13:59:11 +01:00
}
2019-11-11 04:24:53 +01:00
export default async function ( {
2019-09-05 01:56:11 +02:00
path ,
pathMap ,
distDir ,
buildId ,
outDir ,
2020-01-15 02:22:15 +01:00
pagesDataDir ,
2019-09-05 01:56:11 +02:00
renderOpts ,
2019-09-24 10:50:04 +02:00
buildExport ,
2019-09-05 01:56:11 +02:00
serverRuntimeConfig ,
subFolders ,
2019-11-11 04:24:53 +01:00
serverless ,
2019-09-05 01:56:11 +02:00
} ) {
let results = {
2019-11-11 04:24:53 +01:00
ampValidations : [ ] ,
2019-09-05 01:56:11 +02:00
}
2019-03-20 04:53:47 +01:00
2019-09-05 01:56:11 +02:00
try {
2020-01-17 03:39:00 +01:00
const { query : originalQuery = { } } = pathMap
2019-09-24 10:50:04 +02:00
const { page } = pathMap
2019-09-05 01:56:11 +02:00
const filePath = path === '/' ? '/index' : path
const ampPath = ` ${ filePath } .amp `
2020-01-17 03:39:00 +01:00
let query = { ... originalQuery }
2019-12-14 07:31:48 +01:00
let params
2019-09-05 01:56:11 +02:00
2020-01-17 03:39:00 +01:00
// We need to show a warning if they try to provide query values
// for an auto-exported page since they won't be available
const hasOrigQueryValues = Object . keys ( originalQuery ) . length > 0
const queryWithAutoExportWarn = ( ) => {
if ( hasOrigQueryValues ) {
throw new Error (
` \n Error: you provided query values for ${ path } which is an auto-exported page. These can not be applied since the page can no longer be re-rendered on the server. To disable auto-export for this page add \` getInitialProps \` \n `
)
}
}
2019-09-05 01:56:11 +02:00
// Check if the page is a specified dynamic route
if ( isDynamicRoute ( page ) && page !== path ) {
2019-12-14 07:31:48 +01:00
params = getRouteMatcher ( getRouteRegex ( page ) ) ( path )
2019-09-05 01:56:11 +02:00
if ( params ) {
2019-12-14 07:31:48 +01:00
// we have to pass these separately for serverless
if ( ! serverless ) {
query = {
... query ,
... params ,
}
2019-05-22 18:36:53 +02:00
}
2019-09-05 01:56:11 +02:00
} else {
throw new Error (
` The provided export path ' ${ path } ' doesn't match the ' ${ page } ' page. \n Read more: https://err.sh/zeit/next.js/export-path-mismatch `
)
}
}
2019-05-22 18:36:53 +02:00
2019-09-05 01:56:11 +02:00
const headerMocks = {
headers : { } ,
getHeader : ( ) => ( { } ) ,
setHeader : ( ) => { } ,
hasHeader : ( ) => false ,
removeHeader : ( ) => { } ,
2019-11-11 04:24:53 +01:00
getHeaderNames : ( ) => [ ] ,
2019-09-05 01:56:11 +02:00
}
2019-05-22 18:36:53 +02:00
2019-09-05 01:56:11 +02:00
const req = {
url : path ,
2019-11-11 04:24:53 +01:00
... headerMocks ,
2019-09-05 01:56:11 +02:00
}
const res = {
2019-11-11 04:24:53 +01:00
... headerMocks ,
2019-09-05 01:56:11 +02:00
}
2019-08-06 22:26:01 +02:00
2019-09-05 01:56:11 +02:00
envConfig . setConfig ( {
serverRuntimeConfig ,
2019-11-11 04:24:53 +01:00
publicRuntimeConfig : renderOpts . runtimeConfig ,
2019-09-05 01:56:11 +02:00
} )
let htmlFilename = ` ${ filePath } ${ sep } index.html `
if ( ! subFolders ) htmlFilename = ` ${ filePath } .html `
const pageExt = extname ( page )
const pathExt = extname ( path )
// Make sure page isn't a folder with a dot in the name e.g. `v1.2`
if ( pageExt !== pathExt && pathExt !== '' ) {
// If the path has an extension, use that as the filename instead
htmlFilename = path
} else if ( path === '/' ) {
// If the path is the root, just use index.html
htmlFilename = 'index.html'
}
2019-08-06 22:26:01 +02:00
2019-09-05 01:56:11 +02:00
const baseDir = join ( outDir , dirname ( htmlFilename ) )
2019-10-30 17:29:23 +01:00
let htmlFilepath = join ( outDir , htmlFilename )
2019-09-05 01:56:11 +02:00
await mkdirp ( baseDir )
let html
let curRenderOpts = { }
let renderMethod = renderToHTML
2019-09-24 10:50:04 +02:00
// eslint-disable-next-line camelcase
const renderedDuringBuild = unstable _getStaticProps => {
// eslint-disable-next-line camelcase
return ! buildExport && unstable _getStaticProps && ! isDynamicRoute ( path )
}
2019-09-05 01:56:11 +02:00
if ( serverless ) {
2019-12-14 07:31:48 +01:00
const curUrl = url . parse ( req . url , true )
req . url = url . format ( {
... curUrl ,
query : {
... curUrl . query ,
... query ,
} ,
} )
const { Component : mod } = await loadComponents (
2019-09-05 01:56:11 +02:00
distDir ,
2019-12-14 07:31:48 +01:00
buildId ,
page ,
serverless
)
// if it was auto-exported the HTML is loaded here
if ( typeof mod === 'string' ) {
html = mod
2020-01-17 03:39:00 +01:00
queryWithAutoExportWarn ( )
2019-12-14 07:31:48 +01:00
} else {
2020-01-15 02:22:15 +01:00
// for non-dynamic SSG pages we should have already
2019-12-14 07:31:48 +01:00
// prerendered the file
if ( renderedDuringBuild ( mod . unstable _getStaticProps ) ) return results
if ( mod . unstable _getStaticProps && ! htmlFilepath . endsWith ( '.html' ) ) {
// make sure it ends with .html if the name contains a dot
htmlFilename += '.html'
htmlFilepath += '.html'
}
2019-10-30 17:29:23 +01:00
2019-12-14 07:31:48 +01:00
renderMethod = mod . renderReqToHTML
const result = await renderMethod ( req , res , true , { ampPath } , params )
curRenderOpts = result . renderOpts || { }
html = result . html
}
2019-09-14 01:02:15 +02:00
if ( ! html ) {
throw new Error ( ` Failed to render serverless page ` )
}
2019-09-05 01:56:11 +02:00
} else {
const components = await loadComponents (
distDir ,
buildId ,
page ,
serverless
)
2020-01-15 02:22:15 +01:00
// for non-dynamic SSG pages we should have already
2019-09-24 10:50:04 +02:00
// prerendered the file
if ( renderedDuringBuild ( components . unstable _getStaticProps ) ) {
return results
}
2019-10-30 17:29:23 +01:00
// TODO: de-dupe the logic here between serverless and server mode
if (
components . unstable _getStaticProps &&
! htmlFilepath . endsWith ( '.html' )
) {
// make sure it ends with .html if the name contains a dot
htmlFilepath += '.html'
htmlFilename += '.html'
}
2019-09-05 01:56:11 +02:00
if ( typeof components . Component === 'string' ) {
html = components . Component
2020-01-17 03:39:00 +01:00
queryWithAutoExportWarn ( )
2019-09-05 01:56:11 +02:00
} else {
curRenderOpts = { ... components , ... renderOpts , ampPath }
html = await renderMethod ( req , res , page , query , curRenderOpts )
}
}
2018-12-12 13:59:11 +01:00
2019-11-26 10:47:55 +01:00
const validateAmp = async ( html , page , validatorPath ) => {
const validator = await AmpHtmlValidator . getInstance ( validatorPath )
2019-09-05 01:56:11 +02:00
const result = validator . validateString ( html )
const errors = result . errors . filter ( e => e . severity === 'ERROR' )
const warnings = result . errors . filter ( e => e . severity !== 'ERROR' )
if ( warnings . length || errors . length ) {
results . ampValidations . push ( {
page ,
result : {
errors ,
2019-11-11 04:24:53 +01:00
warnings ,
} ,
2019-09-05 01:56:11 +02:00
} )
}
}
2019-05-22 18:36:53 +02:00
2019-09-05 01:56:11 +02:00
if ( curRenderOpts . inAmpMode ) {
2019-11-26 10:47:55 +01:00
await validateAmp ( html , path , curRenderOpts . ampValidator )
2019-09-05 01:56:11 +02:00
} else if ( curRenderOpts . hybridAmp ) {
// we need to render the AMP version
let ampHtmlFilename = ` ${ ampPath } ${ sep } index.html `
if ( ! subFolders ) {
ampHtmlFilename = ` ${ ampPath } .html `
}
const ampBaseDir = join ( outDir , dirname ( ampHtmlFilename ) )
const ampHtmlFilepath = join ( outDir , ampHtmlFilename )
try {
await accessP ( ampHtmlFilepath )
} catch ( _ ) {
// make sure it doesn't exist from manual mapping
let ampHtml
2019-05-22 18:36:53 +02:00
if ( serverless ) {
2019-09-05 01:56:11 +02:00
req . url += ( req . url . includes ( '?' ) ? '&' : '?' ) + 'amp=1'
ampHtml = ( await renderMethod ( req , res , true ) ) . html
2019-05-22 18:36:53 +02:00
} else {
2019-09-05 01:56:11 +02:00
ampHtml = await renderMethod (
req ,
res ,
2019-05-23 09:52:36 +02:00
page ,
2019-09-05 01:56:11 +02:00
{ ... query , amp : 1 } ,
curRenderOpts
2019-05-23 09:52:36 +02:00
)
2019-05-22 18:36:53 +02:00
}
2019-03-26 22:21:27 +01:00
2019-09-05 01:56:11 +02:00
await validateAmp ( ampHtml , page + '?amp=1' )
await mkdirp ( ampBaseDir )
await writeFileP ( ampHtmlFilepath , ampHtml , 'utf8' )
2018-12-12 13:59:11 +01:00
}
}
2019-09-24 10:50:04 +02:00
2020-01-15 02:22:15 +01:00
if ( curRenderOpts . pageData ) {
2019-09-24 10:50:04 +02:00
const dataFile = join (
2020-01-15 02:22:15 +01:00
pagesDataDir ,
2019-09-24 10:50:04 +02:00
htmlFilename . replace ( /\.html$/ , '.json' )
)
await mkdirp ( dirname ( dataFile ) )
2020-01-15 02:22:15 +01:00
await writeFileP ( dataFile , JSON . stringify ( curRenderOpts . pageData ) , 'utf8' )
2019-09-24 10:50:04 +02:00
}
results . fromBuildExportRevalidate = curRenderOpts . revalidate
2019-09-05 01:56:11 +02:00
await writeFileP ( htmlFilepath , html , 'utf8' )
return results
} catch ( error ) {
2019-11-27 10:54:57 +01:00
console . error (
` \n Error occurred prerendering page " ${ path } " https://err.sh/zeit/next.js/prerender-error: ` ,
error
)
2019-09-05 01:56:11 +02:00
return { ... results , error : true }
2018-12-12 13:59:11 +01:00
}
2019-09-05 01:56:11 +02:00
}