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-05-02 06:10:19 +02:00
import { promises } 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'
2020-05-12 21:58:21 +02:00
import 'next/dist/next-server/server/node-polyfill-fetch'
2020-05-26 10:50:51 +02:00
import { IncomingMessage , ServerResponse } from 'http'
import { ComponentType } from 'react'
import { GetStaticProps } from '../types'
2020-07-28 12:19:28 +02:00
import { requireFontManifest } from '../next-server/server/require'
import { FontManifest } from '../next-server/server/font-utils'
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-02-22 17:33:28 +01:00
2020-05-26 10:50:51 +02:00
; ( global as any ) . __NEXT_DATA__ = {
2019-11-11 04:24:53 +01:00
nextExport : true ,
2018-12-12 13:59:11 +01:00
}
2020-05-26 10:50:51 +02:00
interface AmpValidation {
page : string
result : {
errors : AmpHtmlValidator.ValidationError [ ]
warnings : AmpHtmlValidator.ValidationError [ ]
}
}
interface PathMap {
page : string
query ? : { [ key : string ] : string | string [ ] }
}
2020-06-15 16:41:17 +02:00
interface ExportPageInput {
2020-05-26 10:50:51 +02:00
path : string
pathMap : PathMap
distDir : string
outDir : string
pagesDataDir : string
renderOpts : RenderOpts
buildExport? : boolean
serverRuntimeConfig : string
subFolders : string
serverless : boolean
2020-07-28 12:19:28 +02:00
optimizeFonts : boolean
2020-08-05 19:49:44 +02:00
optimizeImages : boolean
2020-05-26 10:50:51 +02:00
}
interface ExportPageResults {
ampValidations : AmpValidation [ ]
fromBuildExportRevalidate? : number
error? : boolean
}
interface RenderOpts {
runtimeConfig ? : { [ key : string ] : any }
params ? : { [ key : string ] : string | string [ ] }
ampPath? : string
ampValidatorPath? : string
ampSkipValidation? : boolean
hybridAmp? : boolean
inAmpMode? : boolean
2020-07-28 12:19:28 +02:00
optimizeFonts? : boolean
2020-08-05 19:49:44 +02:00
optimizeImages? : boolean
2020-07-28 12:19:28 +02:00
fontManifest? : FontManifest
2020-05-26 10:50:51 +02:00
}
type ComponentModule = ComponentType < { } > & {
renderReqToHTML : typeof renderToHTML
getStaticProps? : GetStaticProps
}
export default async function exportPage ( {
2019-09-05 01:56:11 +02:00
path ,
pathMap ,
distDir ,
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 ,
2020-07-28 12:19:28 +02:00
optimizeFonts ,
2020-08-05 19:49:44 +02:00
optimizeImages ,
2020-06-15 16:41:17 +02:00
} : ExportPageInput ) : Promise < ExportPageResults > {
2020-05-26 10:50:51 +02:00
let results : ExportPageResults = {
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 }
2020-05-26 10:50:51 +02:00
let params : { [ key : string ] : string | string [ ] } | undefined
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 (
` \ nError: 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 ) {
2020-05-26 10:50:51 +02:00
params = getRouteMatcher ( getRouteRegex ( page ) ) ( path ) || undefined
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 (
2020-05-27 23:51:11 +02:00
` The provided export path ' ${ path } ' doesn't match the ' ${ page } ' page. \ nRead more: https://err.sh/vercel/next.js/export-path-mismatch `
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 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
2020-05-26 10:50:51 +02:00
const req = ( {
2019-09-05 01:56:11 +02:00
url : path ,
2019-11-11 04:24:53 +01:00
. . . headerMocks ,
2020-05-26 10:50:51 +02:00
} as unknown ) as IncomingMessage
const res = ( {
2019-11-11 04:24:53 +01:00
. . . headerMocks ,
2020-05-26 10:50:51 +02:00
} as unknown ) as ServerResponse
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-05-02 06:10:19 +02:00
await promises . mkdir ( baseDir , { recursive : true } )
2019-09-05 01:56:11 +02:00
let html
2020-05-26 10:50:51 +02:00
let curRenderOpts : RenderOpts = { }
2019-09-05 01:56:11 +02:00
let renderMethod = renderToHTML
2020-05-26 10:50:51 +02:00
const renderedDuringBuild = ( getStaticProps : any ) = > {
2020-02-27 18:57:39 +01:00
return ! buildExport && getStaticProps && ! isDynamicRoute ( path )
2019-09-24 10:50:04 +02:00
}
2019-09-05 01:56:11 +02:00
if ( serverless ) {
2020-05-26 10:50:51 +02:00
const curUrl = url . parse ( req . url ! , true )
2019-12-14 07:31:48 +01:00
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
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-05-26 10:50:51 +02:00
if ( renderedDuringBuild ( ( mod as ComponentModule ) . getStaticProps ) )
return results
2019-12-14 07:31:48 +01:00
2020-05-26 10:50:51 +02:00
if (
( mod as ComponentModule ) . 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
2020-05-26 10:50:51 +02:00
renderMethod = ( mod as ComponentModule ) . renderReqToHTML
2020-03-09 18:30:44 +01:00
const result = await renderMethod (
req ,
res ,
'export' ,
2020-07-28 12:19:28 +02:00
{
ampPath ,
/// @ts-ignore
optimizeFonts ,
2020-08-05 19:49:44 +02:00
/// @ts-ignore
optimizeImages ,
2020-07-28 12:19:28 +02:00
fontManifest : optimizeFonts
? requireFontManifest ( distDir , serverless )
: null ,
} ,
2020-05-26 10:50:51 +02:00
// @ts-ignore
2020-03-09 18:30:44 +01:00
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 {
2020-06-15 16:41:17 +02:00
const components = await loadComponents ( distDir , page , serverless )
2019-09-05 01:56:11 +02:00
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-07-28 12:19:28 +02:00
/ * *
* This sets environment variable to be used at the time of static export by head . tsx .
* Using this from process . env allows targetting both serverless and SSR by calling
* ` process.env.__NEXT_OPTIMIZE_FONTS ` .
* TODO ( prateekbh @ ) : Remove this when experimental . optimizeFonts are being clened up .
* /
if ( optimizeFonts ) {
process . env . __NEXT_OPTIMIZE_FONTS = JSON . stringify ( true )
}
2020-08-05 19:49:44 +02:00
if ( optimizeImages ) {
process . env . __NEXT_OPTIMIZE_IMAGES = JSON . stringify ( true )
}
2020-07-28 12:19:28 +02:00
curRenderOpts = {
. . . components ,
. . . renderOpts ,
ampPath ,
params ,
optimizeFonts ,
2020-08-05 19:49:44 +02:00
optimizeImages ,
2020-07-28 12:19:28 +02:00
fontManifest : optimizeFonts
? requireFontManifest ( distDir , serverless )
: null ,
}
2020-05-26 10:50:51 +02:00
// @ts-ignore
2019-09-05 01:56:11 +02:00
html = await renderMethod ( req , res , page , query , curRenderOpts )
}
}
2018-12-12 13:59:11 +01:00
2020-05-26 10:50:51 +02:00
const validateAmp = async (
2020-06-01 23:00:22 +02:00
rawAmpHtml : string ,
ampPageName : string ,
2020-05-26 10:50:51 +02:00
validatorPath? : string
) = > {
2019-11-26 10:47:55 +01:00
const validator = await AmpHtmlValidator . getInstance ( validatorPath )
2020-06-01 23:00:22 +02:00
const result = validator . validateString ( rawAmpHtml )
2020-05-18 21:24:37 +02:00
const errors = result . errors . filter ( ( e ) = > e . severity === 'ERROR' )
const warnings = result . errors . filter ( ( e ) = > e . severity !== 'ERROR' )
2019-09-05 01:56:11 +02:00
if ( warnings . length || errors . length ) {
results . ampValidations . push ( {
2020-06-01 23:00:22 +02:00
page : ampPageName ,
2019-09-05 01:56:11 +02:00
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 {
2020-05-02 06:10:19 +02:00
await promises . access ( ampHtmlFilepath )
2019-09-05 01:56:11 +02:00
} catch ( _ ) {
// make sure it doesn't exist from manual mapping
let ampHtml
2019-05-22 18:36:53 +02:00
if ( serverless ) {
2020-05-26 10:50:51 +02:00
req . url += ( req . url ! . includes ( '?' ) ? '&' : '?' ) + 'amp=1'
// @ts-ignore
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 ,
2020-05-26 10:50:51 +02:00
// @ts-ignore
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-05-02 06:10:19 +02:00
await promises . mkdir ( ampBaseDir , { recursive : true } )
await promises . writeFile ( ampHtmlFilepath , ampHtml , 'utf8' )
2018-12-12 13:59:11 +01:00
}
}
2019-09-24 10:50:04 +02:00
2020-05-26 10:50:51 +02:00
if ( ( curRenderOpts as any ) . 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-05-02 06:10:19 +02:00
await promises . mkdir ( dirname ( dataFile ) , { recursive : true } )
await promises . writeFile (
dataFile ,
2020-05-26 10:50:51 +02:00
JSON . stringify ( ( curRenderOpts as any ) . pageData ) ,
2020-05-02 06:10:19 +02:00
'utf8'
)
2020-05-19 13:58:50 +02:00
if ( curRenderOpts . hybridAmp ) {
await promises . writeFile (
dataFile . replace ( /\.json$/ , '.amp.json' ) ,
2020-05-26 10:50:51 +02:00
JSON . stringify ( ( curRenderOpts as any ) . pageData ) ,
2020-05-19 13:58:50 +02:00
'utf8'
)
}
2019-09-24 10:50:04 +02:00
}
2020-05-26 10:50:51 +02:00
results . fromBuildExportRevalidate = ( curRenderOpts as any ) . revalidate
2019-09-24 10:50:04 +02:00
2020-05-02 06:10:19 +02:00
await promises . writeFile ( htmlFilepath , html , 'utf8' )
2019-09-05 01:56:11 +02:00
return results
} catch ( error ) {
2019-11-27 10:54:57 +01:00
console . error (
2020-05-13 06:18:38 +02:00
` \ nError occurred prerendering page " ${ path } ". Read more: https://err.sh/next.js/prerender-error \ n ` +
2020-06-01 19:08:34 +02:00
error . stack
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
}