2019-02-14 16:22:57 +01:00
import { IncomingMessage , ServerResponse } from 'http'
2018-12-13 01:00:46 +01:00
import { ParsedUrlQuery } from 'querystring'
import React from 'react'
import { renderToString , renderToStaticMarkup } from 'react-dom/server'
2019-04-22 19:55:03 +02:00
import { IRouterInterface } from '../lib/router/router'
import mitt , { MittEmitter } from '../lib/mitt' ;
import { loadGetInitialProps , isResSent , getDisplayName , ComponentsEnhancer , RenderPage , IDocumentInitialProps , NextComponentType , DocumentType , AppType } from '../lib/utils'
2018-12-13 01:00:46 +01:00
import Head , { defaultHead } from '../lib/head'
import Loadable from '../lib/loadable'
2019-04-02 16:09:34 +02:00
import { DataManagerContext } from '../lib/data-manager-context'
2019-04-05 21:43:40 +02:00
import { RequestContext } from '../lib/request-context'
2019-04-22 19:55:03 +02:00
import { LoadableContext } from '../lib/loadable-context'
2019-04-02 16:09:34 +02:00
import { RouterContext } from '../lib/router-context'
2019-04-05 21:43:40 +02:00
import { DataManager } from '../lib/data-manager'
2019-02-14 16:22:57 +01:00
import {
2019-04-11 20:59:26 +02:00
ManifestItem ,
2019-02-14 16:22:57 +01:00
getDynamicImportBundles ,
Manifest as ReactLoadableManifest ,
} from './get-dynamic-import-bundles'
import { getPageFiles , BuildManifest } from './get-page-files'
2019-04-11 20:59:26 +02:00
import { AmpModeContext } from '../lib/amphtml-context'
2019-04-02 20:01:34 +02:00
import optimizeAmp from './optimize-amp'
2019-04-11 20:59:26 +02:00
import { isAmp } from '../lib/amp' ;
2018-12-13 01:00:46 +01:00
2019-03-07 17:13:38 +01:00
function noRouter() {
const message = 'No router instance found. you should only use "next/router" inside the client side of your app. https://err.sh/zeit/next.js/no-router-instance'
throw new Error ( message )
}
2019-03-13 15:56:20 +01:00
class ServerRouter implements IRouterInterface {
route : string
pathname : string
query : string
asPath : string
// TODO: Remove in the next major version, as this would mean the user is adding event listeners in server-side `render` method
static events : MittEmitter = mitt ( )
constructor ( pathname : string , query : any , as : string ) {
2019-03-23 23:00:46 +01:00
this . route = pathname . replace ( /\/$/ , '' ) || '/'
2019-03-13 15:56:20 +01:00
this . pathname = pathname
this . query = query
this . asPath = as
this . pathname = pathname
}
2019-03-07 17:13:38 +01:00
// @ts-ignore
push() {
noRouter ( )
}
// @ts-ignore
replace() {
noRouter ( )
}
// @ts-ignore
reload() {
noRouter ( )
}
back() {
noRouter ( )
}
// @ts-ignore
prefetch() {
noRouter ( )
}
beforePopState() {
noRouter ( )
}
}
2019-02-14 16:22:57 +01:00
function enhanceComponents (
options : ComponentsEnhancer ,
2019-04-22 19:55:03 +02:00
App : AppType ,
Component : NextComponentType ,
2019-02-14 16:22:57 +01:00
) : {
2019-04-22 19:55:03 +02:00
App : AppType ,
Component : NextComponentType ,
2018-12-13 01:00:46 +01:00
} {
// For backwards compatibility
2019-02-08 11:57:29 +01:00
if ( typeof options === 'function' ) {
2018-12-13 01:00:46 +01:00
return {
2019-02-08 11:57:29 +01:00
App ,
Component : options ( Component ) ,
2018-12-13 01:00:46 +01:00
}
}
return {
App : options.enhanceApp ? options . enhanceApp ( App ) : App ,
2019-02-14 16:22:57 +01:00
Component : options.enhanceComponent
? options . enhanceComponent ( Component )
: Component ,
2018-12-13 01:00:46 +01:00
}
}
2019-02-14 16:22:57 +01:00
function render (
renderElementToString : ( element : React.ReactElement < any > ) = > string ,
element : React.ReactElement < any > ,
2019-04-22 19:55:03 +02:00
) : { html : string ; head : React.ReactElement [ ] } {
2018-12-13 01:00:46 +01:00
let html
let head
try {
html = renderElementToString ( element )
} finally {
head = Head . rewind ( ) || defaultHead ( )
}
return { html , head }
}
type RenderOpts = {
2019-04-02 16:09:34 +02:00
ampBindInitData : boolean
2019-02-14 16:22:57 +01:00
staticMarkup : boolean
buildId : string
2019-04-04 23:54:01 +02:00
dynamicBuildId? : boolean
2019-02-14 16:22:57 +01:00
runtimeConfig ? : { [ key : string ] : any }
2019-04-15 11:26:23 +02:00
dangerousAsPath : string
2019-02-14 16:22:57 +01:00
assetPrefix? : string
err? : Error | null
nextExport? : boolean
dev? : boolean
2019-03-20 04:53:47 +01:00
ampPath? : string
2019-02-14 16:22:57 +01:00
amphtml? : boolean
2019-04-02 16:09:34 +02:00
hasAmp? : boolean ,
2019-04-13 02:04:52 +02:00
ampMode? : any ,
2019-04-02 16:09:34 +02:00
dataOnly? : boolean ,
2019-02-14 16:22:57 +01:00
buildManifest : BuildManifest
reactLoadableManifest : ReactLoadableManifest
Component : React.ComponentType
2019-04-22 19:55:03 +02:00
Document : DocumentType
App : AppType
2019-02-14 16:22:57 +01:00
ErrorDebug? : React.ComponentType < { error : Error } > ,
2019-04-11 20:59:26 +02:00
ampValidator ? : ( html : string , pathname : string ) = > Promise < void > ,
2018-12-13 01:00:46 +01:00
}
2019-02-14 16:22:57 +01:00
function renderDocument (
2019-04-22 19:55:03 +02:00
Document : DocumentType ,
2019-02-14 16:22:57 +01:00
{
2019-04-02 16:09:34 +02:00
dataManagerData ,
2019-02-14 16:22:57 +01:00
props ,
docProps ,
pathname ,
query ,
buildId ,
2019-04-04 23:54:01 +02:00
dynamicBuildId = false ,
2019-02-14 16:22:57 +01:00
assetPrefix ,
runtimeConfig ,
nextExport ,
dynamicImportsIds ,
2019-04-15 11:26:23 +02:00
dangerousAsPath ,
2019-02-14 16:22:57 +01:00
err ,
dev ,
2019-03-20 04:53:47 +01:00
ampPath ,
2019-02-14 16:22:57 +01:00
amphtml ,
2019-03-19 20:01:42 +01:00
hasAmp ,
2019-04-13 02:04:52 +02:00
ampMode ,
2019-02-14 16:22:57 +01:00
staticMarkup ,
devFiles ,
files ,
dynamicImports ,
} : RenderOpts & {
2019-04-22 19:55:03 +02:00
dataManagerData : string ,
2019-02-14 16:22:57 +01:00
props : any
2019-04-22 19:55:03 +02:00
docProps : IDocumentInitialProps
2019-02-14 16:22:57 +01:00
pathname : string
query : ParsedUrlQuery
2019-04-15 11:26:23 +02:00
dangerousAsPath : string
2019-03-20 04:53:47 +01:00
ampPath : string ,
2019-02-14 16:22:57 +01:00
amphtml : boolean
2019-03-19 20:01:42 +01:00
hasAmp : boolean ,
2019-04-13 02:04:52 +02:00
ampMode : any ,
2019-02-14 16:22:57 +01:00
dynamicImportsIds : string [ ]
dynamicImports : ManifestItem [ ]
files : string [ ]
devFiles : string [ ] ,
} ,
) : string {
return (
'<!DOCTYPE html>' +
renderToStaticMarkup (
2019-04-13 02:04:52 +02:00
< AmpModeContext.Provider value = { ampMode } >
< Document
__NEXT_DATA__ = { {
dataManager : dataManagerData ,
props , // The result of getInitialProps
page : pathname , // The rendered page
query , // querystring parsed / passed by the user
buildId , // buildId is used to facilitate caching of page bundles, we send it to the client so that pageloader knows where to load bundles
dynamicBuildId , // Specifies if the buildId should by dynamically fetched
assetPrefix : assetPrefix === '' ? undefined : assetPrefix , // send assetPrefix to the client side when configured, otherwise don't sent in the resulting HTML
runtimeConfig , // runtimeConfig if provided, otherwise don't sent in the resulting HTML
nextExport , // If this is a page exported by `next export`
2019-04-22 19:55:03 +02:00
dynamicIds : dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds ,
2019-04-13 02:04:52 +02:00
err : err ? serializeError ( dev , err ) : undefined , // Error if one happened, otherwise don't sent in the resulting HTML
} }
2019-04-15 11:26:23 +02:00
dangerousAsPath = { dangerousAsPath }
2019-04-13 02:04:52 +02:00
ampPath = { ampPath }
amphtml = { amphtml }
hasAmp = { hasAmp }
staticMarkup = { staticMarkup }
devFiles = { devFiles }
files = { files }
dynamicImports = { dynamicImports }
assetPrefix = { assetPrefix }
{ . . . docProps }
/ >
< / AmpModeContext.Provider > ,
2019-02-14 16:22:57 +01:00
)
2018-12-13 01:00:46 +01:00
)
}
2019-02-14 16:22:57 +01:00
export async function renderToHTML (
req : IncomingMessage ,
res : ServerResponse ,
pathname : string ,
query : ParsedUrlQuery ,
renderOpts : RenderOpts ,
) : Promise < string | null > {
2019-03-12 12:40:49 +01:00
pathname = pathname === '/index' ? '/' : pathname
2018-12-13 01:00:46 +01:00
const {
err ,
dev = false ,
2019-04-02 16:09:34 +02:00
ampBindInitData = false ,
2018-12-18 17:12:49 +01:00
staticMarkup = false ,
2019-03-20 04:53:47 +01:00
ampPath = '' ,
2018-12-18 17:12:49 +01:00
App ,
Document ,
Component ,
buildManifest ,
reactLoadableManifest ,
2019-02-08 11:57:29 +01:00
ErrorDebug ,
2018-12-13 01:00:46 +01:00
} = renderOpts
2018-12-18 17:12:49 +01:00
2018-12-13 01:00:46 +01:00
await Loadable . preloadAll ( ) // Make sure all dynamic imports are loaded
2018-12-17 16:09:23 +01:00
if ( dev ) {
const { isValidElementType } = require ( 'react-is' )
if ( ! isValidElementType ( Component ) ) {
2019-02-14 16:22:57 +01:00
throw new Error (
` The default export is not a React Component in page: " ${ pathname } " ` ,
)
2018-12-17 16:09:23 +01:00
}
2018-12-17 17:42:40 +01:00
if ( ! isValidElementType ( App ) ) {
2019-02-14 16:22:57 +01:00
throw new Error (
` The default export is not a React Component in page: "/_app" ` ,
)
2018-12-17 17:42:40 +01:00
}
if ( ! isValidElementType ( Document ) ) {
2019-02-14 16:22:57 +01:00
throw new Error (
` The default export is not a React Component in page: "/_document" ` ,
)
2018-12-17 17:42:40 +01:00
}
}
2018-12-13 01:00:46 +01:00
2019-03-13 15:56:20 +01:00
// @ts-ignore url will always be set
const asPath : string = req . url
2018-12-13 01:00:46 +01:00
const ctx = { err , req , res , pathname , query , asPath }
2019-03-07 17:13:38 +01:00
const router = new ServerRouter ( pathname , query , asPath )
2019-03-17 17:43:03 +01:00
let props : any
try {
props = await loadGetInitialProps ( App , { Component , router , ctx } )
} catch ( err ) {
if ( ! dev || ! err ) throw err
ctx . err = err
renderOpts . err = err
}
2018-12-13 01:00:46 +01:00
2018-12-17 16:09:23 +01:00
// the response might be finished on the getInitialProps call
2018-12-13 01:00:46 +01:00
if ( isResSent ( res ) ) return null
const devFiles = buildManifest . devFiles
const files = [
. . . new Set ( [
. . . getPageFiles ( buildManifest , pathname ) ,
. . . getPageFiles ( buildManifest , '/_app' ) ,
2019-02-08 11:57:29 +01:00
] ) ,
2018-12-13 01:00:46 +01:00
]
2019-04-02 16:09:34 +02:00
let dataManager : DataManager | undefined
if ( ampBindInitData ) {
dataManager = new DataManager ( )
}
2018-12-13 01:00:46 +01:00
const reactLoadableModules : string [ ] = [ ]
2019-04-02 16:09:34 +02:00
const renderElementToString = staticMarkup
? renderToStaticMarkup
: renderToString
2019-04-03 00:32:07 +02:00
const renderPageError = ( ) : { html : string , head : any } | void = > {
if ( ctx . err && ErrorDebug ) {
return render ( renderElementToString , < ErrorDebug error = { ctx . err } / > )
}
if ( dev && ( props . router || props . Component ) ) {
throw new Error (
` 'router' and 'Component' can not be returned in getInitialProps from _app.js https://err.sh/zeit/next.js/cant-override-next-props.md ` ,
)
}
}
2019-04-22 19:55:03 +02:00
let renderPage : RenderPage
2019-04-02 16:09:34 +02:00
2019-04-11 20:59:26 +02:00
const ampMode = {
enabled : false ,
2019-04-15 11:26:23 +02:00
hasQuery : Boolean ( query . amp && /^(y|yes|true|1)/i . test ( query . amp . toString ( ) ) ) ,
2019-04-11 20:59:26 +02:00
}
2019-04-02 16:09:34 +02:00
if ( ampBindInitData ) {
2019-04-21 20:47:02 +02:00
const ssrPrepass = require ( 'react-ssr-prepass' )
2019-04-02 16:09:34 +02:00
renderPage = async (
options : ComponentsEnhancer = { } ,
2019-04-05 12:32:00 +02:00
) : Promise < { html : string ; head : any , dataOnly? : true } > = > {
2019-04-03 00:32:07 +02:00
const renderError = renderPageError ( )
if ( renderError ) return renderError
2019-04-02 16:09:34 +02:00
const {
App : EnhancedApp ,
Component : EnhancedComponent ,
} = enhanceComponents ( options , App , Component )
2019-04-05 21:43:40 +02:00
const Application = ( ) = > < RequestContext.Provider value = { req } >
< RouterContext.Provider value = { router } >
< DataManagerContext.Provider value = { dataManager } >
2019-04-11 20:59:26 +02:00
< AmpModeContext.Provider value = { ampMode } >
2019-04-05 21:43:40 +02:00
< LoadableContext.Provider
value = { ( moduleName ) = > reactLoadableModules . push ( moduleName ) }
>
< EnhancedApp
Component = { EnhancedComponent }
router = { router }
{ . . . props }
/ >
< / LoadableContext.Provider >
2019-04-11 20:59:26 +02:00
< / AmpModeContext.Provider >
2019-04-05 21:43:40 +02:00
< / DataManagerContext.Provider >
< / RouterContext.Provider >
< / RequestContext.Provider >
2019-04-05 12:32:00 +02:00
const element = < Application / >
2019-04-05 19:43:30 +02:00
try {
return render (
renderElementToString ,
element ,
)
} catch ( err ) {
if ( err && typeof err === 'object' && typeof err . then === 'function' ) {
await ssrPrepass ( element )
if ( renderOpts . dataOnly ) {
return {
html : '' ,
head : [ ] ,
dataOnly : true ,
}
} else {
return render (
renderElementToString ,
element ,
)
}
2019-04-02 16:09:34 +02:00
}
2019-04-05 19:43:30 +02:00
throw err
2019-04-02 16:09:34 +02:00
}
}
} else {
renderPage = (
2019-04-03 00:32:07 +02:00
options : ComponentsEnhancer = { } ,
) : { html : string ; head : any } = > {
const renderError = renderPageError ( )
if ( renderError ) return renderError
2018-12-13 01:00:46 +01:00
2019-04-03 00:32:07 +02:00
const {
App : EnhancedApp ,
Component : EnhancedComponent ,
} = enhanceComponents ( options , App , Component )
return render (
renderElementToString ,
2019-04-05 21:43:40 +02:00
< RequestContext.Provider value = { req } >
< RouterContext.Provider value = { router } >
2019-04-11 20:59:26 +02:00
< AmpModeContext.Provider value = { ampMode } >
2019-04-05 21:43:40 +02:00
< LoadableContext.Provider
value = { ( moduleName ) = > reactLoadableModules . push ( moduleName ) }
>
< EnhancedApp
Component = { EnhancedComponent }
router = { router }
{ . . . props }
/ >
< / LoadableContext.Provider >
2019-04-11 20:59:26 +02:00
< / AmpModeContext.Provider >
2019-04-05 21:43:40 +02:00
< / RouterContext.Provider >
< / RequestContext.Provider > ,
2019-03-01 20:51:13 +01:00
)
2019-03-01 18:08:27 +01:00
}
2019-04-02 16:09:34 +02:00
}
2018-12-13 01:00:46 +01:00
const docProps = await loadGetInitialProps ( Document , { . . . ctx , renderPage } )
2019-01-02 20:21:57 +01:00
// the response might be finished on the getInitialProps call
2018-12-13 01:00:46 +01:00
if ( isResSent ( res ) ) return null
2019-04-02 16:09:34 +02:00
let dataManagerData = '[]'
if ( dataManager ) {
dataManagerData = JSON . stringify ( [ . . . dataManager . getData ( ) ] )
}
2019-04-22 19:55:03 +02:00
if ( ! docProps || typeof docProps . html !== 'string' ) {
const message = ` " ${ getDisplayName ( Document ) } .getInitialProps()" should resolve to an object with a "html" prop set with a valid html string `
throw new Error ( message )
}
2019-04-05 12:32:00 +02:00
if ( docProps . dataOnly ) {
2019-04-02 16:09:34 +02:00
return dataManagerData
}
2019-04-05 12:32:00 +02:00
const dynamicImports = [
. . . getDynamicImportBundles ( reactLoadableManifest , reactLoadableModules ) ,
]
const dynamicImportsIds : any = dynamicImports . map ( ( bundle ) = > bundle . id )
2019-04-11 20:59:26 +02:00
const amphtml = isAmp ( ampMode )
const hasAmp = ! amphtml && ampMode . enabled
// update renderOpts so export knows it's AMP
renderOpts . amphtml = amphtml
renderOpts . hasAmp = hasAmp
2019-04-05 12:32:00 +02:00
2019-04-02 20:01:34 +02:00
let html = renderDocument ( Document , {
2018-12-13 01:00:46 +01:00
. . . renderOpts ,
2019-04-15 11:26:23 +02:00
dangerousAsPath : router.asPath ,
2019-04-02 16:09:34 +02:00
dataManagerData ,
2019-04-13 02:04:52 +02:00
ampMode ,
2018-12-13 01:00:46 +01:00
props ,
docProps ,
pathname ,
2019-03-20 04:53:47 +01:00
ampPath ,
2019-02-14 16:22:57 +01:00
amphtml ,
2019-03-19 20:01:42 +01:00
hasAmp ,
2018-12-13 01:00:46 +01:00
query ,
dynamicImportsIds ,
dynamicImports ,
files ,
2019-02-08 11:57:29 +01:00
devFiles ,
2018-12-13 01:00:46 +01:00
} )
2019-04-02 20:01:34 +02:00
2019-04-11 20:59:26 +02:00
if ( amphtml && html ) {
2019-04-20 03:33:20 +02:00
if ( ampMode . hasQuery ) {
html = await optimizeAmp ( html , { amphtml , query } )
}
2019-04-11 20:59:26 +02:00
2019-04-20 03:33:20 +02:00
if ( renderOpts . ampValidator ) {
2019-04-11 20:59:26 +02:00
await renderOpts . ampValidator ( html , pathname )
}
2019-04-20 03:33:20 +02:00
// run optimize after validating in dirty mode
if ( ! ampMode . hasQuery ) {
html = await optimizeAmp ( html , { amphtml , query } )
}
2019-04-02 20:01:34 +02:00
}
2019-04-16 15:57:17 +02:00
if ( amphtml || hasAmp ) {
// fix & being escaped for amphtml rel link
html = html . replace ( /&amp=1/g , '&=1' )
}
2019-04-02 20:01:34 +02:00
return html
2018-12-13 01:00:46 +01:00
}
2019-02-08 11:57:29 +01:00
function errorToJSON ( err : Error ) : Error {
2018-12-13 01:00:46 +01:00
const { name , message , stack } = err
return { name , message , stack }
}
2019-02-14 16:22:57 +01:00
function serializeError (
dev : boolean | undefined ,
err : Error ,
) : Error & { statusCode? : number } {
2018-12-13 01:00:46 +01:00
if ( dev ) {
return errorToJSON ( err )
}
2019-02-14 16:22:57 +01:00
return {
name : 'Internal Server Error.' ,
message : '500 - Internal Server Error.' ,
statusCode : 500 ,
}
2018-12-13 01:00:46 +01:00
}