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-03-13 15:56:20 +01:00
import { IRouterInterface } from '../lib/router/router'
import mitt , { MittEmitter } from '../lib/mitt' ;
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'
2019-04-02 16:09:34 +02:00
import { DataManagerContext } from '../lib/data-manager-context'
2019-03-23 21:00:31 +01:00
import { LoadableContext } from '../lib/loadable-context'
2019-04-02 16:09:34 +02:00
import { RouterContext } from '../lib/router-context'
import { DataManager } from '..//lib/data-manager'
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 )
}
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 ,
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
2019-04-02 16:09:34 +02:00
ampBindInitData : boolean
2019-02-14 16:22:57 +01:00
staticMarkup : boolean
buildId : string
runtimeConfig ? : { [ key : string ] : any }
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 ,
dataOnly? : boolean ,
2019-02-14 16:22:57 +01:00
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 ,
{
2019-04-02 16:09:34 +02:00
dataManagerData ,
2019-02-14 16:22:57 +01:00
ampEnabled = false ,
props ,
docProps ,
pathname ,
query ,
buildId ,
assetPrefix ,
runtimeConfig ,
nextExport ,
dynamicImportsIds ,
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-02-14 16:22:57 +01:00
staticMarkup ,
devFiles ,
files ,
dynamicImports ,
} : RenderOpts & {
2019-04-02 16:09:34 +02:00
dataManagerData : any ,
2019-02-14 16:22:57 +01:00
props : any
docProps : any
pathname : string
query : ParsedUrlQuery
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-02-14 16:22:57 +01:00
dynamicImportsIds : string [ ]
dynamicImports : ManifestItem [ ]
files : string [ ]
devFiles : string [ ] ,
} ,
) : string {
return (
'<!DOCTYPE html>' +
renderToStaticMarkup (
< IsAmpContext.Provider value = { amphtml } >
< Document
__NEXT_DATA__ = { {
2019-04-02 16:09:34 +02:00
dataManager : dataManagerData ,
2019-02-14 16:22:57 +01:00
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 :
2019-04-02 16:09:34 +02:00
dynamicImportsIds . length === 0 ? undefined : dynamicImportsIds ,
2019-02-14 16:22:57 +01:00
err : err ? serializeError ( dev , err ) : undefined , // Error if one happened, otherwise don't sent in the resulting HTML
} }
ampEnabled = { ampEnabled }
2019-03-20 04:53:47 +01:00
ampPath = { ampPath }
2019-02-14 16:22:57 +01:00
amphtml = { amphtml }
2019-03-19 20:01:42 +01:00
hasAmp = { hasAmp }
2019-02-14 16:22:57 +01:00
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 > {
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-02-14 16:22:57 +01:00
amphtml = false ,
2019-03-19 20:01:42 +01:00
hasAmp = 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
let renderPage : ( options : ComponentsEnhancer ) = > { html : string , head : any } | Promise < { html : string ; head : any } >
if ( ampBindInitData ) {
renderPage = async (
options : ComponentsEnhancer = { } ,
) : Promise < { html : string ; head : any } > = > {
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 ` ,
)
}
const {
App : EnhancedApp ,
Component : EnhancedComponent ,
} = enhanceComponents ( options , App , Component )
let recursionCount = 0
const renderTree = async ( ) : Promise < any > = > {
recursionCount ++
// This is temporary, we can remove it once the API is finished.
if ( recursionCount > 100 ) {
throw new Error ( 'Did 100 promise recursions, bailing out to avoid infinite loop.' )
}
try {
return await render (
renderElementToString ,
< RouterContext.Provider value = { router } >
< DataManagerContext.Provider value = { dataManager } >
< IsAmpContext.Provider value = { amphtml } >
< LoadableContext.Provider
value = { ( moduleName ) = > reactLoadableModules . push ( moduleName ) }
>
< EnhancedApp
Component = { EnhancedComponent }
router = { router }
{ . . . props }
/ >
< / LoadableContext.Provider >
< / IsAmpContext.Provider >
< / DataManagerContext.Provider >
< / RouterContext.Provider > ,
)
} catch ( err ) {
if ( typeof err . then !== 'undefined' ) {
await err
return await renderTree ( )
}
throw err
}
}
const res = await renderTree ( )
return res
}
} else {
renderPage = (
2019-02-14 16:22:57 +01:00
options : ComponentsEnhancer = { } ,
) : { html : string ; head : any } = > {
2019-03-17 17:43:03 +01:00
if ( ctx . err && ErrorDebug ) {
return render ( renderElementToString , < ErrorDebug error = { ctx . err } / > )
2018-12-13 01:00:46 +01:00
}
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 ,
2019-04-02 16:09:34 +02:00
< RouterContext.Provider value = { router } >
< IsAmpContext.Provider value = { amphtml } >
< LoadableContext.Provider
value = { ( moduleName ) = > reactLoadableModules . push ( moduleName ) }
>
< EnhancedApp
Component = { EnhancedComponent }
router = { router }
{ . . . props }
/ >
< / LoadableContext.Provider >
< / IsAmpContext.Provider >
< / RouterContext.Provider > ,
2018-12-13 01:00:46 +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-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 )
2019-04-02 16:09:34 +02:00
let dataManagerData = '[]'
if ( dataManager ) {
dataManagerData = JSON . stringify ( [ . . . dataManager . getData ( ) ] )
}
if ( renderOpts . dataOnly ) {
return dataManagerData
}
2018-12-13 01:00:46 +01:00
return renderDocument ( Document , {
. . . renderOpts ,
2019-04-02 16:09:34 +02:00
dataManagerData ,
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-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
}