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-01-14 15:41:09 +01:00
import Router from '../lib/router/router'
2018-12-13 01:00:46 +01:00
import { loadGetInitialProps , isResSent } from '../lib/utils'
import Head , { defaultHead } from '../lib/head'
import Loadable from '../lib/loadable'
import LoadableCapture from '../lib/loadable-capture'
2019-02-14 16:22:57 +01:00
import {
getDynamicImportBundles ,
Manifest as ReactLoadableManifest ,
ManifestItem ,
} from './get-dynamic-import-bundles'
import { getPageFiles , BuildManifest } from './get-page-files'
import { IsAmpContext } from '../lib/amphtml-context'
2018-12-13 01:00:46 +01:00
type Enhancer = ( Component : React.ComponentType ) = > React . ComponentType
2019-02-14 16:22:57 +01:00
type ComponentsEnhancer =
| { enhanceApp? : Enhancer ; enhanceComponent? : Enhancer }
| Enhancer
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 )
}
class ServerRouter extends Router {
// @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 ,
2018-12-13 01:00:46 +01:00
App : React.ComponentType ,
2019-02-08 11:57:29 +01:00
Component : React.ComponentType ,
2019-02-14 16:22:57 +01:00
) : {
App : React.ComponentType
Component : React.ComponentType ,
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 > ,
) : { html : string ; head : any } {
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-02-14 16:22:57 +01:00
ampEnabled : boolean
staticMarkup : boolean
buildId : string
runtimeConfig ? : { [ key : string ] : any }
assetPrefix? : string
err? : Error | null
nextExport? : boolean
dev? : boolean
amphtml? : boolean
buildManifest : BuildManifest
reactLoadableManifest : ReactLoadableManifest
Component : React.ComponentType
Document : React.ComponentType
App : React.ComponentType
ErrorDebug? : React.ComponentType < { error : Error } > ,
2018-12-13 01:00:46 +01:00
}
2019-02-14 16:22:57 +01:00
function renderDocument (
Document : React.ComponentType ,
{
ampEnabled = false ,
props ,
docProps ,
pathname ,
query ,
buildId ,
assetPrefix ,
runtimeConfig ,
nextExport ,
dynamicImportsIds ,
err ,
dev ,
amphtml ,
staticMarkup ,
devFiles ,
files ,
dynamicImports ,
} : RenderOpts & {
props : any
docProps : any
pathname : string
query : ParsedUrlQuery
amphtml : boolean
dynamicImportsIds : string [ ]
dynamicImports : ManifestItem [ ]
files : string [ ]
devFiles : string [ ] ,
} ,
) : string {
return (
'<!DOCTYPE html>' +
renderToStaticMarkup (
< IsAmpContext.Provider value = { amphtml } >
< Document
__NEXT_DATA__ = { {
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
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`
dynamicIds :
dynamicImportsIds . length === 0 ? undefined : dynamicImportsIds ,
err : err ? serializeError ( dev , err ) : undefined , // Error if one happened, otherwise don't sent in the resulting HTML
} }
ampEnabled = { ampEnabled }
amphtml = { amphtml }
staticMarkup = { staticMarkup }
devFiles = { devFiles }
files = { files }
dynamicImports = { dynamicImports }
assetPrefix = { assetPrefix }
{ . . . docProps }
/ >
< / IsAmpContext.Provider > ,
)
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 > {
2018-12-13 01:00:46 +01:00
const {
err ,
dev = false ,
2018-12-18 17:12:49 +01:00
staticMarkup = false ,
2019-02-14 16:22:57 +01:00
amphtml = false ,
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
const asPath = req . url
const ctx = { err , req , res , pathname , query , asPath }
2019-03-07 17:13:38 +01:00
const router = new ServerRouter ( pathname , query , asPath )
2019-02-14 16:22:57 +01:00
const props = await loadGetInitialProps ( App , { Component , router , ctx } )
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
]
const reactLoadableModules : string [ ] = [ ]
2019-02-14 16:22:57 +01:00
const renderPage = (
options : ComponentsEnhancer = { } ,
) : { html : string ; head : any } = > {
const renderElementToString = staticMarkup
? renderToStaticMarkup
: renderToString
2018-12-13 01:00:46 +01:00
2019-02-08 11:57:29 +01:00
if ( err && ErrorDebug ) {
2018-12-13 01:00:46 +01:00
return render ( renderElementToString , < ErrorDebug error = { err } / > )
}
2019-03-01 18:08:27 +01:00
if ( dev && ( props . router || props . Component ) ) {
2019-03-01 20:51:13 +01:00
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-03-01 18:08:27 +01:00
}
2019-02-14 16:22:57 +01:00
const {
App : EnhancedApp ,
Component : EnhancedComponent ,
} = enhanceComponents ( options , App , Component )
2018-12-18 17:12:49 +01:00
2019-02-14 16:22:57 +01:00
return render (
renderElementToString ,
< IsAmpContext.Provider value = { amphtml } >
< LoadableCapture
report = { ( moduleName ) = > reactLoadableModules . push ( moduleName ) }
>
< EnhancedApp
Component = { EnhancedComponent }
router = { router }
{ . . . props }
/ >
< / LoadableCapture >
< / IsAmpContext.Provider > ,
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-02-14 16:22:57 +01:00
const dynamicImports = [
. . . getDynamicImportBundles ( reactLoadableManifest , reactLoadableModules ) ,
]
2018-12-13 01:00:46 +01:00
const dynamicImportsIds : any = dynamicImports . map ( ( bundle ) = > bundle . id )
return renderDocument ( Document , {
. . . renderOpts ,
props ,
docProps ,
pathname ,
2019-02-14 16:22:57 +01:00
amphtml ,
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-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
}