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-07-11 19:35:39 +02:00
|
|
|
import { NextRouter } from '../lib/router/router'
|
2019-05-30 03:19:32 +02:00
|
|
|
import mitt, { MittEmitter } from '../lib/mitt'
|
|
|
|
import {
|
|
|
|
loadGetInitialProps,
|
|
|
|
isResSent,
|
|
|
|
getDisplayName,
|
|
|
|
ComponentsEnhancer,
|
|
|
|
RenderPage,
|
|
|
|
DocumentInitialProps,
|
|
|
|
NextComponentType,
|
|
|
|
DocumentType,
|
|
|
|
AppType,
|
|
|
|
NextPageContext,
|
|
|
|
} from '../lib/utils'
|
2018-12-13 01:00:46 +01:00
|
|
|
import Head, { defaultHead } from '../lib/head'
|
2019-04-24 16:47:50 +02:00
|
|
|
// @ts-ignore types will be added later as it's an internal module
|
2018-12-13 01:00:46 +01:00
|
|
|
import Loadable from '../lib/loadable'
|
2019-04-02 16:09:34 +02:00
|
|
|
import { DataManagerContext } from '../lib/data-manager-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 { getPageFiles, BuildManifest } from './get-page-files'
|
2019-06-27 16:22:24 +02:00
|
|
|
import { AmpStateContext } from '../lib/amp-context'
|
2019-04-02 20:01:34 +02:00
|
|
|
import optimizeAmp from './optimize-amp'
|
2019-06-27 16:22:24 +02:00
|
|
|
import { isInAmpMode } from '../lib/amp'
|
2019-09-04 16:00:54 +02:00
|
|
|
// Uses a module path because of the compiled output directory location
|
|
|
|
import { PageConfig } from 'next/types'
|
2018-12-13 01:00:46 +01:00
|
|
|
|
2019-06-29 02:48:28 +02:00
|
|
|
export type ManifestItem = {
|
|
|
|
id: number | string
|
|
|
|
name: string
|
|
|
|
file: string
|
|
|
|
publicPath: string
|
|
|
|
}
|
|
|
|
|
|
|
|
type ReactLoadableManifest = { [moduleId: string]: ManifestItem[] }
|
|
|
|
|
2019-03-07 17:13:38 +01:00
|
|
|
function noRouter() {
|
2019-05-30 03:19:32 +02:00
|
|
|
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'
|
2019-03-07 17:13:38 +01:00
|
|
|
throw new Error(message)
|
|
|
|
}
|
|
|
|
|
2019-07-11 19:35:39 +02:00
|
|
|
class ServerRouter implements NextRouter {
|
2019-03-13 15:56:20 +01:00
|
|
|
route: string
|
|
|
|
pathname: string
|
2019-05-20 17:21:15 +02:00
|
|
|
query: ParsedUrlQuery
|
2019-03-13 15:56:20 +01:00
|
|
|
asPath: string
|
2019-07-11 19:35:39 +02:00
|
|
|
events: any
|
2019-03-13 15:56:20 +01:00
|
|
|
// 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()
|
|
|
|
|
2019-05-20 17:21:15 +02:00
|
|
|
constructor(pathname: string, query: ParsedUrlQuery, 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
|
|
|
|
}
|
2019-07-11 19:35:39 +02:00
|
|
|
push(): any {
|
2019-03-07 17:13:38 +01:00
|
|
|
noRouter()
|
|
|
|
}
|
2019-07-11 19:35:39 +02:00
|
|
|
replace(): any {
|
2019-03-07 17:13:38 +01:00
|
|
|
noRouter()
|
|
|
|
}
|
|
|
|
reload() {
|
|
|
|
noRouter()
|
|
|
|
}
|
|
|
|
back() {
|
|
|
|
noRouter()
|
|
|
|
}
|
2019-07-11 19:35:39 +02:00
|
|
|
prefetch(): any {
|
2019-03-07 17:13:38 +01:00
|
|
|
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,
|
2019-05-30 03:19:32 +02:00
|
|
|
Component: NextComponentType
|
2019-02-14 16:22:57 +01:00
|
|
|
): {
|
2019-05-30 03:19:32 +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-05-30 03:19:32 +02:00
|
|
|
ampMode: 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 {
|
2019-07-25 18:39:09 +02:00
|
|
|
head = Head.rewind() || defaultHead(isInAmpMode(ampMode))
|
2018-12-13 01:00:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return { html, head }
|
|
|
|
}
|
|
|
|
|
|
|
|
type RenderOpts = {
|
2019-06-24 17:31:27 +02:00
|
|
|
documentMiddlewareEnabled: boolean
|
2019-04-02 16:09:34 +02:00
|
|
|
ampBindInitData: boolean
|
2019-02-14 16:22:57 +01:00
|
|
|
staticMarkup: boolean
|
|
|
|
buildId: string
|
2019-05-29 02:32:18 +02:00
|
|
|
canonicalBase: string
|
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
|
2019-09-17 22:05:20 +02:00
|
|
|
hasCssMode: boolean
|
2019-02-14 16:22:57 +01:00
|
|
|
err?: Error | null
|
2019-09-15 20:35:14 +02:00
|
|
|
autoExport?: boolean
|
2019-02-14 16:22:57 +01:00
|
|
|
nextExport?: boolean
|
2019-08-06 22:26:01 +02:00
|
|
|
skeleton?: boolean
|
2019-02-14 16:22:57 +01:00
|
|
|
dev?: boolean
|
2019-05-30 03:19:32 +02:00
|
|
|
ampMode?: any
|
2019-06-27 16:22:24 +02:00
|
|
|
ampPath?: string
|
2019-05-30 03:19:32 +02:00
|
|
|
dataOnly?: boolean
|
2019-06-27 16:22:24 +02:00
|
|
|
inAmpMode?: boolean
|
|
|
|
hybridAmp?: boolean
|
2019-02-14 16:22:57 +01:00
|
|
|
buildManifest: BuildManifest
|
|
|
|
reactLoadableManifest: ReactLoadableManifest
|
2019-07-01 23:13:52 +02:00
|
|
|
pageConfig: PageConfig
|
2019-02-14 16:22:57 +01:00
|
|
|
Component: React.ComponentType
|
2019-04-22 19:55:03 +02:00
|
|
|
Document: DocumentType
|
2019-05-24 04:05:08 +02:00
|
|
|
DocumentMiddleware: (ctx: NextPageContext) => void
|
2019-04-22 19:55:03 +02:00
|
|
|
App: AppType
|
2019-05-30 03:19:32 +02:00
|
|
|
ErrorDebug?: React.ComponentType<{ error: Error }>
|
|
|
|
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-05-29 02:32:18 +02:00
|
|
|
canonicalBase,
|
2019-02-14 16:22:57 +01:00
|
|
|
assetPrefix,
|
|
|
|
runtimeConfig,
|
|
|
|
nextExport,
|
2019-09-15 20:35:14 +02:00
|
|
|
autoExport,
|
2019-08-06 22:26:01 +02:00
|
|
|
skeleton,
|
2019-02-14 16:22:57 +01:00
|
|
|
dynamicImportsIds,
|
2019-04-15 11:26:23 +02:00
|
|
|
dangerousAsPath,
|
2019-09-17 22:05:20 +02:00
|
|
|
hasCssMode,
|
2019-02-14 16:22:57 +01:00
|
|
|
err,
|
|
|
|
dev,
|
2019-03-20 04:53:47 +01:00
|
|
|
ampPath,
|
2019-06-27 16:22:24 +02:00
|
|
|
ampState,
|
|
|
|
inAmpMode,
|
|
|
|
hybridAmp,
|
2019-02-14 16:22:57 +01:00
|
|
|
staticMarkup,
|
|
|
|
devFiles,
|
|
|
|
files,
|
|
|
|
dynamicImports,
|
|
|
|
}: RenderOpts & {
|
2019-05-30 03:19:32 +02:00
|
|
|
dataManagerData: string
|
2019-02-14 16:22:57 +01:00
|
|
|
props: any
|
2019-04-26 09:37:57 +02:00
|
|
|
docProps: DocumentInitialProps
|
2019-02-14 16:22:57 +01:00
|
|
|
pathname: string
|
|
|
|
query: ParsedUrlQuery
|
2019-04-15 11:26:23 +02:00
|
|
|
dangerousAsPath: string
|
2019-06-27 16:22:24 +02:00
|
|
|
ampState: any
|
2019-05-30 03:19:32 +02:00
|
|
|
ampPath: string
|
2019-06-27 16:22:24 +02:00
|
|
|
inAmpMode: boolean
|
|
|
|
hybridAmp: boolean
|
2019-02-14 16:22:57 +01:00
|
|
|
dynamicImportsIds: string[]
|
|
|
|
dynamicImports: ManifestItem[]
|
|
|
|
files: string[]
|
2019-05-30 03:19:32 +02:00
|
|
|
devFiles: string[]
|
|
|
|
}
|
2019-02-14 16:22:57 +01:00
|
|
|
): string {
|
|
|
|
return (
|
|
|
|
'<!DOCTYPE html>' +
|
|
|
|
renderToStaticMarkup(
|
2019-06-27 16:22:24 +02:00
|
|
|
<AmpStateContext.Provider value={ampState}>
|
2019-04-13 02:04:52 +02:00
|
|
|
<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
|
|
|
|
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-09-15 20:35:14 +02:00
|
|
|
autoExport, // If this is an auto exported page
|
2019-08-06 22:26:01 +02:00
|
|
|
skeleton, // If this is a skeleton page for experimentalPrerender
|
2019-05-30 03:19:32 +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-05-29 02:32:18 +02:00
|
|
|
canonicalBase={canonicalBase}
|
2019-04-13 02:04:52 +02:00
|
|
|
ampPath={ampPath}
|
2019-06-27 16:22:24 +02:00
|
|
|
inAmpMode={inAmpMode}
|
2019-09-17 22:05:20 +02:00
|
|
|
isDevelopment={!!dev}
|
|
|
|
hasCssMode={hasCssMode}
|
2019-06-27 16:22:24 +02:00
|
|
|
hybridAmp={hybridAmp}
|
2019-04-13 02:04:52 +02:00
|
|
|
staticMarkup={staticMarkup}
|
|
|
|
devFiles={devFiles}
|
|
|
|
files={files}
|
|
|
|
dynamicImports={dynamicImports}
|
|
|
|
assetPrefix={assetPrefix}
|
|
|
|
{...docProps}
|
|
|
|
/>
|
2019-06-27 16:22:24 +02:00
|
|
|
</AmpStateContext.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,
|
2019-05-30 03:19:32 +02:00
|
|
|
renderOpts: RenderOpts
|
2019-02-14 16:22:57 +01:00
|
|
|
): 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-06-24 17:31:27 +02:00
|
|
|
documentMiddlewareEnabled = 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,
|
2019-07-16 02:06:16 +02:00
|
|
|
pageConfig = {},
|
2019-05-24 04:05:08 +02:00
|
|
|
DocumentMiddleware,
|
2018-12-18 17:12:49 +01:00
|
|
|
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
|
2019-09-15 20:35:14 +02:00
|
|
|
|
|
|
|
const defaultAppGetInitialProps =
|
|
|
|
App.getInitialProps === (App as any).origGetInitialProps
|
|
|
|
|
|
|
|
let isAutoExport =
|
|
|
|
typeof (Component as any).getInitialProps !== 'function' &&
|
|
|
|
defaultAppGetInitialProps
|
|
|
|
|
|
|
|
let isPrerender = pageConfig.experimentalPrerender === true
|
|
|
|
const isStaticPage = isPrerender || isAutoExport
|
|
|
|
// TODO: revisit `?_nextPreviewSkeleton=(truthy)`
|
|
|
|
const isSkeleton = isPrerender && !!query._nextPreviewSkeleton
|
|
|
|
// remove from query so it doesn't end up in document
|
|
|
|
delete query._nextPreviewSkeleton
|
2018-12-13 01:00:46 +01:00
|
|
|
|
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(
|
2019-05-30 03:19:32 +02:00
|
|
|
`The default export is not a React Component in page: "${pathname}"`
|
2019-02-14 16:22:57 +01:00
|
|
|
)
|
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(
|
2019-05-30 03:19:32 +02:00
|
|
|
`The default export is not a React Component in page: "/_app"`
|
2019-02-14 16:22:57 +01:00
|
|
|
)
|
2018-12-17 17:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!isValidElementType(Document)) {
|
2019-02-14 16:22:57 +01:00
|
|
|
throw new Error(
|
2019-05-30 03:19:32 +02:00
|
|
|
`The default export is not a React Component in page: "/_document"`
|
2019-02-14 16:22:57 +01:00
|
|
|
)
|
2018-12-17 17:42:40 +01:00
|
|
|
}
|
2019-05-22 18:36:53 +02:00
|
|
|
|
2019-06-28 22:01:11 +02:00
|
|
|
if (isStaticPage) {
|
|
|
|
// remove query values except ones that will be set during export
|
|
|
|
query = {
|
|
|
|
amp: query.amp,
|
2019-05-22 18:36:53 +02:00
|
|
|
}
|
2019-08-16 18:53:47 +02:00
|
|
|
req.url = pathname
|
2019-06-28 22:01:11 +02:00
|
|
|
renderOpts.nextExport = true
|
2019-05-22 18:36:53 +02:00
|
|
|
}
|
2018-12-17 17:42:40 +01:00
|
|
|
}
|
2019-08-12 03:56:57 +02:00
|
|
|
if (isSkeleton) renderOpts.nextExport = true
|
2019-09-15 20:35:14 +02:00
|
|
|
if (isAutoExport) renderOpts.autoExport = true
|
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
|
2019-08-13 11:33:48 +02:00
|
|
|
const router = new ServerRouter(pathname, query, asPath)
|
2019-05-31 02:34:05 +02:00
|
|
|
const ctx = {
|
|
|
|
err,
|
|
|
|
req: isStaticPage ? undefined : req,
|
|
|
|
res: isStaticPage ? undefined : res,
|
|
|
|
pathname,
|
|
|
|
query,
|
|
|
|
asPath,
|
2019-08-13 11:33:48 +02:00
|
|
|
AppTree: (props: any) => {
|
|
|
|
return (
|
|
|
|
<AppContainer>
|
|
|
|
<App {...props} Component={Component} router={router} />
|
|
|
|
</AppContainer>
|
|
|
|
)
|
|
|
|
},
|
2019-05-31 02:34:05 +02:00
|
|
|
}
|
2019-08-13 11:33:48 +02:00
|
|
|
let props: any
|
2019-08-12 03:56:57 +02:00
|
|
|
const isDataPrerender =
|
|
|
|
pageConfig.experimentalPrerender === true &&
|
|
|
|
req.headers['content-type'] === 'application/json'
|
2019-03-17 17:43:03 +01:00
|
|
|
|
2019-06-24 17:31:27 +02:00
|
|
|
if (documentMiddlewareEnabled && typeof DocumentMiddleware === 'function') {
|
2019-05-24 04:05:08 +02:00
|
|
|
await DocumentMiddleware(ctx)
|
|
|
|
}
|
|
|
|
|
2019-07-03 04:16:12 +02:00
|
|
|
let dataManager: DataManager | undefined
|
|
|
|
if (ampBindInitData) {
|
|
|
|
dataManager = new DataManager()
|
|
|
|
}
|
|
|
|
|
|
|
|
const ampState = {
|
|
|
|
ampFirst: pageConfig.amp === true,
|
|
|
|
hasQuery: Boolean(query.amp),
|
|
|
|
hybrid: pageConfig.amp === 'hybrid',
|
|
|
|
}
|
|
|
|
|
|
|
|
const reactLoadableModules: string[] = []
|
|
|
|
|
|
|
|
const AppContainer = ({ children }: any) => (
|
2019-07-30 20:00:19 +02:00
|
|
|
<RouterContext.Provider value={router}>
|
|
|
|
<DataManagerContext.Provider value={dataManager}>
|
|
|
|
<AmpStateContext.Provider value={ampState}>
|
|
|
|
<LoadableContext.Provider
|
|
|
|
value={moduleName => reactLoadableModules.push(moduleName)}
|
|
|
|
>
|
|
|
|
{children}
|
|
|
|
</LoadableContext.Provider>
|
|
|
|
</AmpStateContext.Provider>
|
|
|
|
</DataManagerContext.Provider>
|
|
|
|
</RouterContext.Provider>
|
2019-07-03 04:16:12 +02:00
|
|
|
)
|
|
|
|
|
2019-03-17 17:43:03 +01:00
|
|
|
try {
|
2019-08-12 03:56:57 +02:00
|
|
|
props =
|
|
|
|
isSkeleton && !isDataPrerender
|
|
|
|
? { pageProps: {} }
|
|
|
|
: await loadGetInitialProps(App, {
|
2019-08-22 19:06:30 +02:00
|
|
|
AppTree: ctx.AppTree,
|
2019-08-12 03:56:57 +02:00
|
|
|
Component,
|
|
|
|
router,
|
|
|
|
ctx,
|
|
|
|
})
|
2019-03-17 17:43:03 +01:00
|
|
|
} catch (err) {
|
|
|
|
if (!dev || !err) throw err
|
|
|
|
ctx.err = err
|
|
|
|
renderOpts.err = err
|
|
|
|
}
|
2018-12-13 01:00:46 +01:00
|
|
|
|
2019-08-12 03:56:57 +02:00
|
|
|
if (isDataPrerender) {
|
2019-08-06 22:26:01 +02:00
|
|
|
res.setHeader('content-type', 'application/json')
|
|
|
|
res.end(JSON.stringify(props.pageProps || {}))
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
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
|
|
|
const renderElementToString = staticMarkup
|
|
|
|
? renderToStaticMarkup
|
|
|
|
: renderToString
|
|
|
|
|
2019-05-30 03:19:32 +02:00
|
|
|
const renderPageError = (): { html: string; head: any } | void => {
|
2019-04-03 00:32:07 +02:00
|
|
|
if (ctx.err && ErrorDebug) {
|
2019-05-30 03:19:32 +02:00
|
|
|
return render(
|
|
|
|
renderElementToString,
|
|
|
|
<ErrorDebug error={ctx.err} />,
|
2019-06-27 16:22:24 +02:00
|
|
|
ampState
|
2019-05-30 03:19:32 +02:00
|
|
|
)
|
2019-04-03 00:32:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (dev && (props.router || props.Component)) {
|
|
|
|
throw new Error(
|
2019-05-30 03:19:32 +02:00
|
|
|
`'router' and 'Component' can not be returned in getInitialProps from _app.js https://err.sh/zeit/next.js/cant-override-next-props`
|
2019-04-03 00:32:07 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-22 19:55:03 +02:00
|
|
|
let renderPage: RenderPage
|
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 (
|
2019-05-30 03:19:32 +02:00
|
|
|
options: ComponentsEnhancer = {}
|
|
|
|
): 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-05-30 03:19:32 +02:00
|
|
|
const Application = () => (
|
2019-07-03 04:16:12 +02:00
|
|
|
<AppContainer>
|
|
|
|
<EnhancedApp
|
|
|
|
Component={EnhancedComponent}
|
|
|
|
router={router}
|
|
|
|
{...props}
|
|
|
|
/>
|
|
|
|
</AppContainer>
|
2019-05-30 03:19:32 +02:00
|
|
|
)
|
2019-04-05 12:32:00 +02:00
|
|
|
|
2019-05-30 03:19:32 +02:00
|
|
|
const element = <Application />
|
2019-04-05 19:43:30 +02:00
|
|
|
|
|
|
|
try {
|
2019-06-27 16:22:24 +02:00
|
|
|
return render(renderElementToString, element, ampState)
|
2019-04-05 19:43:30 +02:00
|
|
|
} catch (err) {
|
|
|
|
if (err && typeof err === 'object' && typeof err.then === 'function') {
|
|
|
|
await ssrPrepass(element)
|
|
|
|
if (renderOpts.dataOnly) {
|
|
|
|
return {
|
|
|
|
html: '',
|
|
|
|
head: [],
|
|
|
|
dataOnly: true,
|
|
|
|
}
|
|
|
|
} else {
|
2019-06-27 16:22:24 +02:00
|
|
|
return render(renderElementToString, element, ampState)
|
2019-04-05 19:43:30 +02:00
|
|
|
}
|
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-05-30 03:19:32 +02:00
|
|
|
options: ComponentsEnhancer = {}
|
2019-04-03 00:32:07 +02:00
|
|
|
): { 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-07-03 04:16:12 +02:00
|
|
|
<AppContainer>
|
|
|
|
<EnhancedApp
|
|
|
|
Component={EnhancedComponent}
|
|
|
|
router={router}
|
|
|
|
{...props}
|
|
|
|
/>
|
|
|
|
</AppContainer>,
|
2019-06-27 16:22:24 +02:00
|
|
|
ampState
|
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') {
|
2019-05-30 03:19:32 +02:00
|
|
|
const message = `"${getDisplayName(
|
|
|
|
Document
|
|
|
|
)}.getInitialProps()" should resolve to an object with a "html" prop set with a valid html string`
|
2019-04-22 19:55:03 +02:00
|
|
|
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-06-29 02:48:28 +02:00
|
|
|
const dynamicImportIdsSet = new Set<string>()
|
|
|
|
const dynamicImports: ManifestItem[] = []
|
|
|
|
|
|
|
|
for (const mod of reactLoadableModules) {
|
|
|
|
const manifestItem = reactLoadableManifest[mod]
|
|
|
|
|
|
|
|
if (manifestItem) {
|
|
|
|
manifestItem.map(item => {
|
|
|
|
dynamicImports.push(item)
|
|
|
|
dynamicImportIdsSet.add(item.id as string)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const dynamicImportsIds = [...dynamicImportIdsSet]
|
2019-06-27 16:22:24 +02:00
|
|
|
const inAmpMode = isInAmpMode(ampState)
|
|
|
|
const hybridAmp = ampState.hybrid
|
|
|
|
|
2019-07-01 23:13:52 +02:00
|
|
|
// update renderOpts so export knows current state
|
2019-06-27 16:22:24 +02:00
|
|
|
renderOpts.inAmpMode = inAmpMode
|
|
|
|
renderOpts.hybridAmp = hybridAmp
|
2019-08-06 22:26:01 +02:00
|
|
|
if (isSkeleton) renderOpts.skeleton = true
|
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-06-27 16:22:24 +02:00
|
|
|
ampState,
|
2018-12-13 01:00:46 +01:00
|
|
|
props,
|
|
|
|
docProps,
|
|
|
|
pathname,
|
2019-03-20 04:53:47 +01:00
|
|
|
ampPath,
|
2018-12-13 01:00:46 +01:00
|
|
|
query,
|
2019-06-27 16:22:24 +02:00
|
|
|
inAmpMode,
|
|
|
|
hybridAmp,
|
2018-12-13 01:00:46 +01:00
|
|
|
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-06-27 16:22:24 +02:00
|
|
|
if (inAmpMode && html) {
|
2019-04-25 10:14:52 +02:00
|
|
|
// use replace to allow rendering directly to body in AMP mode
|
2019-06-02 11:38:47 +02:00
|
|
|
html = html.replace(
|
|
|
|
'__NEXT_AMP_RENDER_TARGET__',
|
|
|
|
`<!-- __NEXT_DATA__ -->${docProps.html}`
|
|
|
|
)
|
2019-05-30 04:53:41 +02:00
|
|
|
html = await optimizeAmp(html)
|
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-02 20:01:34 +02:00
|
|
|
}
|
2019-04-16 15:57:17 +02:00
|
|
|
|
2019-06-27 16:22:24 +02:00
|
|
|
if (inAmpMode || hybridAmp) {
|
2019-04-16 15:57:17 +02:00
|
|
|
// 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,
|
2019-05-30 03:19:32 +02:00
|
|
|
err: Error
|
2019-02-14 16:22:57 +01:00
|
|
|
): 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
|
|
|
}
|