2019-02-22 17:33:28 +01:00
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'
2020-03-21 17:02:05 +01:00
import { access , mkdir as mkdirOrig , writeFile } from 'fs'
2020-03-29 00:31:06 +01:00
import AmpHtmlValidator from 'next/dist/compiled/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'
2020-02-05 22:10:39 +01:00
import { normalizePagePath } from '../next-server/server/normalize-page-path'
2020-03-18 09:33:10 +01:00
import { SERVER _PROPS _EXPORT _ERROR } from '../lib/constants'
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 )
const accessP = promisify ( access )
2020-03-21 17:02:05 +01:00
const mkdir = promisify ( mkdirOrig )
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
2020-02-05 22:10:39 +01:00
const filePath = normalizePagePath ( path )
2019-09-05 01:56:11 +02:00
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
2020-03-21 17:02:05 +01:00
await mkdir ( baseDir , { recursive : true } )
2019-09-05 01:56:11 +02:00
let html
let curRenderOpts = { }
let renderMethod = renderToHTML
2020-02-27 18:57:39 +01:00
const renderedDuringBuild = getStaticProps => {
return ! buildExport && getStaticProps && ! isDynamicRoute ( path )
2019-09-24 10:50:04 +02:00
}
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 ,
} ,
} )
2020-03-18 09:33:10 +01:00
const { Component : mod , getServerSideProps } = await loadComponents (
2019-09-05 01:56:11 +02:00
distDir ,
2019-12-14 07:31:48 +01:00
buildId ,
page ,
serverless
)
2020-03-18 09:33:10 +01:00
if ( getServerSideProps ) {
throw new Error ( ` Error for page ${ page } : ${ SERVER _PROPS _EXPORT _ERROR } ` )
}
2019-12-14 07:31:48 +01:00
// 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
2020-02-27 18:57:39 +01:00
if ( renderedDuringBuild ( mod . getStaticProps ) ) return results
2019-12-14 07:31:48 +01:00
2020-02-27 18:57:39 +01:00
if ( mod . getStaticProps && ! htmlFilepath . endsWith ( '.html' ) ) {
2019-12-14 07:31:48 +01:00
// 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
2020-03-09 18:30:44 +01:00
const result = await renderMethod (
req ,
res ,
'export' ,
{ ampPath } ,
params
)
2019-12-14 07:31:48 +01:00
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-03-18 09:33:10 +01:00
if ( components . getServerSideProps ) {
throw new Error ( ` Error for page ${ page } : ${ SERVER _PROPS _EXPORT _ERROR } ` )
}
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
2020-02-27 18:57:39 +01:00
if ( renderedDuringBuild ( components . getStaticProps ) ) {
2019-09-24 10:50:04 +02:00
return results
}
2019-10-30 17:29:23 +01:00
// TODO: de-dupe the logic here between serverless and server mode
2020-02-27 18:57:39 +01:00
if ( components . getStaticProps && ! htmlFilepath . endsWith ( '.html' ) ) {
2019-10-30 17:29:23 +01:00
// 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 {
2020-01-27 23:50:59 +01:00
curRenderOpts = { ... components , ... renderOpts , ampPath , params }
2019-09-05 01:56:11 +02:00
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
2020-03-24 09:31:04 +01:00
if ( curRenderOpts . inAmpMode && ! curRenderOpts . ampSkipValidation ) {
2020-02-02 19:02:56 +01:00
await validateAmp ( html , path , curRenderOpts . ampValidatorPath )
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'
2020-03-09 18:30:44 +01:00
ampHtml = ( await renderMethod ( req , res , 'export' ) ) . 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
2020-03-24 09:31:04 +01:00
if ( ! curRenderOpts . ampSkipValidation ) {
await validateAmp ( ampHtml , page + '?amp=1' )
}
2020-03-21 17:02:05 +01:00
await mkdir ( ampBaseDir , { recursive : true } )
2019-09-05 01:56:11 +02:00
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' )
)
2020-03-21 17:02:05 +01:00
await mkdir ( dirname ( dataFile ) , { recursive : true } )
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 (
2020-02-27 13:23:28 +01:00
` \n Error occurred prerendering page " ${ path } ". Read more: https://err.sh/next.js/prerender-error: \n ` +
2020-02-21 06:45:50 +01:00
error
2019-11-27 10:54:57 +01:00
)
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
}