render.tsx 22.1 KB
Newer Older
1
import { IncomingMessage, ServerResponse } from 'http'
2 3
import { ParsedUrlQuery } from 'querystring'
import React from 'react'
J
Joe Haddad 已提交
4
import { renderToStaticMarkup, renderToString } from 'react-dom/server'
J
Joe Haddad 已提交
5
import {
J
Joe Haddad 已提交
6 7 8 9 10
  PAGES_404_GET_INITIAL_PROPS_ERROR,
  SERVER_PROPS_GET_INIT_PROPS_CONFLICT,
  SERVER_PROPS_SSG_CONFLICT,
  SSG_GET_INITIAL_PROPS_CONFLICT,
} from '../../lib/constants'
11
import { isSerializableProps } from '../../lib/is-serializable-props'
J
Joe Haddad 已提交
12 13
import { isInAmpMode } from '../lib/amp'
import { AmpStateContext } from '../lib/amp-context'
14 15 16 17 18
import {
  AMP_RENDER_TARGET,
  STATIC_PROPS_ID,
  SERVER_PROPS_ID,
} from '../lib/constants'
19 20
import Head, { defaultHead } from '../lib/head'
import Loadable from '../lib/loadable'
21
import { LoadableContext } from '../lib/loadable-context'
J
Joe Haddad 已提交
22
import mitt, { MittEmitter } from '../lib/mitt'
T
Tim Neutkens 已提交
23
import { RouterContext } from '../lib/router-context'
J
Joe Haddad 已提交
24
import { NextRouter } from '../lib/router/router'
J
JJ Kasper 已提交
25
import { isDynamicRoute } from '../lib/router/utils/is-dynamic'
26
import {
J
Joe Haddad 已提交
27 28 29 30 31 32 33 34 35 36 37 38
  AppType,
  ComponentsEnhancer,
  DocumentInitialProps,
  DocumentType,
  getDisplayName,
  isResSent,
  loadGetInitialProps,
  NextComponentType,
  RenderPage,
} from '../lib/utils'
import { tryGetPreviewData, __ApiPreviewProps } from './api-utils'
import { getPageFiles } from './get-page-files'
39
import { LoadComponentsReturnType, ManifestItem } from './load-components'
J
Joe Haddad 已提交
40
import optimizeAmp from './optimize-amp'
41

42
function noRouter() {
J
Joe Haddad 已提交
43 44
  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'
45 46 47
  throw new Error(message)
}

48
class ServerRouter implements NextRouter {
49 50
  route: string
  pathname: string
51
  query: ParsedUrlQuery
52
  asPath: string
53
  events: any
54
  isFallback: boolean
55 56 57
  // 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()

58 59 60 61 62 63
  constructor(
    pathname: string,
    query: ParsedUrlQuery,
    as: string,
    { isFallback }: { isFallback: boolean }
  ) {
T
Tim Neutkens 已提交
64
    this.route = pathname.replace(/\/$/, '') || '/'
65 66 67
    this.pathname = pathname
    this.query = query
    this.asPath = as
68 69

    this.isFallback = isFallback
70
  }
71
  push(): any {
72 73
    noRouter()
  }
74
  replace(): any {
75 76 77 78 79 80 81 82
    noRouter()
  }
  reload() {
    noRouter()
  }
  back() {
    noRouter()
  }
83
  prefetch(): any {
84 85 86 87 88 89 90
    noRouter()
  }
  beforePopState() {
    noRouter()
  }
}

91 92
function enhanceComponents(
  options: ComponentsEnhancer,
93
  App: AppType,
J
Joe Haddad 已提交
94
  Component: NextComponentType
95
): {
J
Joe Haddad 已提交
96 97
  App: AppType
  Component: NextComponentType
98 99
} {
  // For backwards compatibility
100
  if (typeof options === 'function') {
101
    return {
102 103
      App,
      Component: options(Component),
104 105 106 107 108
    }
  }

  return {
    App: options.enhanceApp ? options.enhanceApp(App) : App,
109 110 111
    Component: options.enhanceComponent
      ? options.enhanceComponent(Component)
      : Component,
112 113 114
  }
}

115 116 117
function render(
  renderElementToString: (element: React.ReactElement<any>) => string,
  element: React.ReactElement<any>,
J
Joe Haddad 已提交
118
  ampMode: any
119
): { html: string; head: React.ReactElement[] } {
120 121 122 123 124 125
  let html
  let head

  try {
    html = renderElementToString(element)
  } finally {
126
    head = Head.rewind() || defaultHead(isInAmpMode(ampMode))
127 128 129 130 131
  }

  return { html, head }
}

132
export type RenderOptsPartial = {
133 134
  staticMarkup: boolean
  buildId: string
135
  canonicalBase: string
136 137
  runtimeConfig?: { [key: string]: any }
  assetPrefix?: string
J
Joe Haddad 已提交
138
  hasCssMode: boolean
139
  err?: Error | null
140
  autoExport?: boolean
141 142
  nextExport?: boolean
  dev?: boolean
J
Joe Haddad 已提交
143
  ampMode?: any
144 145 146
  ampPath?: string
  inAmpMode?: boolean
  hybridAmp?: boolean
J
Joe Haddad 已提交
147 148
  ErrorDebug?: React.ComponentType<{ error: Error }>
  ampValidator?: (html: string, pathname: string) => Promise<void>
149
  documentMiddlewareEnabled?: boolean
150 151
  isDataReq?: boolean
  params?: ParsedUrlQuery
J
Joe Haddad 已提交
152
  previewProps: __ApiPreviewProps
153 154
}

155 156
export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial

157
function renderDocument(
158
  Document: DocumentType,
159 160 161 162 163 164
  {
    props,
    docProps,
    pathname,
    query,
    buildId,
165
    canonicalBase,
166 167 168
    assetPrefix,
    runtimeConfig,
    nextExport,
169
    autoExport,
170
    isFallback,
171
    dynamicImportsIds,
172
    dangerousAsPath,
J
Joe Haddad 已提交
173
    hasCssMode,
174 175
    err,
    dev,
176
    ampPath,
177 178 179
    ampState,
    inAmpMode,
    hybridAmp,
180 181 182
    staticMarkup,
    devFiles,
    files,
183
    lowPriorityFiles,
184
    polyfillFiles,
185
    dynamicImports,
186 187 188
    htmlProps,
    bodyTags,
    headTags,
189 190
    gsp,
    gssp,
191
    customServer,
192 193
  }: RenderOpts & {
    props: any
194
    docProps: DocumentInitialProps
195 196
    pathname: string
    query: ParsedUrlQuery
197
    dangerousAsPath: string
198
    ampState: any
J
Joe Haddad 已提交
199
    ampPath: string
200 201
    inAmpMode: boolean
    hybridAmp: boolean
202 203
    dynamicImportsIds: string[]
    dynamicImports: ManifestItem[]
J
Joe Haddad 已提交
204
    devFiles: string[]
205 206
    files: string[]
    lowPriorityFiles: string[]
207
    polyfillFiles: string[]
208 209 210
    htmlProps: any
    bodyTags: any
    headTags: any
211
    isFallback?: boolean
212 213
    gsp?: boolean
    gssp?: boolean
214
    customServer?: boolean
J
Joe Haddad 已提交
215
  }
216 217 218 219
): string {
  return (
    '<!DOCTYPE html>' +
    renderToStaticMarkup(
220
      <AmpStateContext.Provider value={ampState}>
G
Gerald Monaco 已提交
221 222
        {Document.renderDocument(Document, {
          __NEXT_DATA__: {
223 224 225 226 227 228 229
            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`
230
            autoExport, // If this is an auto exported page
231
            isFallback,
J
Joe Haddad 已提交
232 233
            dynamicIds:
              dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds,
234
            err: err ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML
235 236
            gsp, // whether the page is getStaticProps
            gssp, // whether the page is getServerSideProps
237
            customServer, // whether the user is using a custom server
G
Gerald Monaco 已提交
238 239 240 241 242 243 244 245 246 247 248
          },
          dangerousAsPath,
          canonicalBase,
          ampPath,
          inAmpMode,
          isDevelopment: !!dev,
          hasCssMode,
          hybridAmp,
          staticMarkup,
          devFiles,
          files,
249
          lowPriorityFiles,
250
          polyfillFiles,
G
Gerald Monaco 已提交
251 252
          dynamicImports,
          assetPrefix,
253 254 255
          htmlProps,
          bodyTags,
          headTags,
G
Gerald Monaco 已提交
256 257
          ...docProps,
        })}
258
      </AmpStateContext.Provider>
259
    )
260 261 262
  )
}

263 264 265 266
const invalidKeysMsg = (methodName: string, invalidKeys: string[]) => {
  return (
    `Additional keys were returned from \`${methodName}\`. Properties intended for your component must be nested under the \`props\` key, e.g.:` +
    `\n\n\treturn { props: { title: 'My Title', content: '...' } }` +
267 268
    `\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.` +
    `\nRead more: https://err.sh/next.js/invalid-getstaticprops-value`
269 270 271
  )
}

272 273 274 275 276
export async function renderToHTML(
  req: IncomingMessage,
  res: ServerResponse,
  pathname: string,
  query: ParsedUrlQuery,
J
Joe Haddad 已提交
277
  renderOpts: RenderOpts
278
): Promise<string | null> {
279
  pathname = pathname === '/index' ? '/' : pathname
280 281 282
  const {
    err,
    dev = false,
283
    documentMiddlewareEnabled = false,
284
    staticMarkup = false,
285
    ampPath = '',
286 287
    App,
    Document,
288
    pageConfig = {},
J
JJ Kasper 已提交
289
    DocumentMiddleware,
290 291 292
    Component,
    buildManifest,
    reactLoadableManifest,
293
    ErrorDebug,
294 295 296
    getStaticProps,
    getStaticPaths,
    getServerSideProps,
297 298
    isDataReq,
    params,
J
Joe Haddad 已提交
299
    previewProps,
300
  } = renderOpts
301

302 303 304 305
  const callMiddleware = async (method: string, args: any[], props = false) => {
    let results: any = props ? {} : []

    if ((Document as any)[`${method}Middleware`]) {
306 307 308 309
      let middlewareFunc = await (Document as any)[`${method}Middleware`]
      middlewareFunc = middlewareFunc.default || middlewareFunc

      const curResults = await middlewareFunc(...args)
310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
      if (props) {
        for (const result of curResults) {
          results = {
            ...results,
            ...result,
          }
        }
      } else {
        results = curResults
      }
    }
    return results
  }

  const headTags = (...args: any) => callMiddleware('headTags', args)
  const bodyTags = (...args: any) => callMiddleware('bodyTags', args)
  const htmlProps = (...args: any) => callMiddleware('htmlProps', args, true)

328
  const didRewrite = (req as any)._nextDidRewrite
329 330 331
  const isFallback = !!query.__nextFallback
  delete query.__nextFallback

332
  const isSSG = !!getStaticProps
333
  const isBuildTimeSSG = isSSG && renderOpts.nextExport
334 335 336
  const defaultAppGetInitialProps =
    App.getInitialProps === (App as any).origGetInitialProps

J
JJ Kasper 已提交
337
  const hasPageGetInitialProps = !!(Component as any).getInitialProps
338

339 340
  const pageIsDynamic = isDynamicRoute(pathname)

J
JJ Kasper 已提交
341
  const isAutoExport =
342 343
    !hasPageGetInitialProps &&
    defaultAppGetInitialProps &&
344
    !isSSG &&
345
    !getServerSideProps
J
JJ Kasper 已提交
346

347 348
  if (
    process.env.NODE_ENV !== 'production' &&
349
    (isAutoExport || isFallback) &&
350
    pageIsDynamic &&
351
    didRewrite
352 353
  ) {
    // TODO: add err.sh when rewrites go stable
354 355 356
    // Behavior might change before then (prefer SSR in this case).
    // If we decide to ship rewrites to the client we could solve this
    // by running over the rewrites and getting the params.
357
    throw new Error(
358 359 360 361 362 363
      `Rewrites don't support${
        isFallback ? ' ' : ' auto-exported '
      }dynamic pages${isFallback ? ' with getStaticProps ' : ' '}yet. ` +
        `Using this will cause the page to fail to parse the params on the client${
          isFallback ? ' for the fallback page ' : ''
        }`
364 365 366
    )
  }

367
  if (hasPageGetInitialProps && isSSG) {
368
    throw new Error(SSG_GET_INITIAL_PROPS_CONFLICT + ` ${pathname}`)
J
JJ Kasper 已提交
369
  }
370

371
  if (hasPageGetInitialProps && getServerSideProps) {
372 373 374
    throw new Error(SERVER_PROPS_GET_INIT_PROPS_CONFLICT + ` ${pathname}`)
  }

375
  if (getServerSideProps && isSSG) {
376 377 378
    throw new Error(SERVER_PROPS_SSG_CONFLICT + ` ${pathname}`)
  }

379
  if (!!getStaticPaths && !isSSG) {
380
    throw new Error(
381
      `getStaticPaths was added without a getStaticProps in ${pathname}. Without getStaticProps, getStaticPaths does nothing`
382 383 384
    )
  }

385
  if (isSSG && pageIsDynamic && !getStaticPaths) {
386
    throw new Error(
387
      `getStaticPaths is required for dynamic SSG pages and is missing for '${pathname}'.` +
388 389 390 391
        `\nRead more: https://err.sh/next.js/invalid-getstaticpaths-value`
    )
  }

392 393 394
  if (dev) {
    const { isValidElementType } = require('react-is')
    if (!isValidElementType(Component)) {
395
      throw new Error(
J
Joe Haddad 已提交
396
        `The default export is not a React Component in page: "${pathname}"`
397
      )
398 399
    }

400
    if (!isValidElementType(App)) {
401
      throw new Error(
J
Joe Haddad 已提交
402
        `The default export is not a React Component in page: "/_app"`
403
      )
404 405 406
    }

    if (!isValidElementType(Document)) {
407
      throw new Error(
J
Joe Haddad 已提交
408
        `The default export is not a React Component in page: "/_document"`
409
      )
410
    }
J
JJ Kasper 已提交
411

J
JJ Kasper 已提交
412
    if (isAutoExport) {
413 414 415
      // remove query values except ones that will be set during export
      query = {
        amp: query.amp,
J
JJ Kasper 已提交
416
      }
417
      req.url = pathname
418
      renderOpts.nextExport = true
J
JJ Kasper 已提交
419
    }
420

421
    if (pathname === '/404' && (hasPageGetInitialProps || getServerSideProps)) {
422 423
      throw new Error(PAGES_404_GET_INITIAL_PROPS_ERROR)
    }
424
  }
425
  if (isAutoExport) renderOpts.autoExport = true
426
  if (isSSG) renderOpts.nextExport = false
427

J
JJ Kasper 已提交
428 429
  await Loadable.preloadAll() // Make sure all dynamic imports are loaded

430 431
  // url will always be set
  const asPath = req.url as string
432 433 434
  const router = new ServerRouter(pathname, query, asPath, {
    isFallback: isFallback,
  })
435 436
  const ctx = {
    err,
J
JJ Kasper 已提交
437 438
    req: isAutoExport ? undefined : req,
    res: isAutoExport ? undefined : res,
439 440 441
    pathname,
    query,
    asPath,
442 443 444 445 446 447 448
    AppTree: (props: any) => {
      return (
        <AppContainer>
          <App {...props} Component={Component} router={router} />
        </AppContainer>
      )
    },
449
  }
450
  let props: any
451

452
  if (documentMiddlewareEnabled && typeof DocumentMiddleware === 'function') {
J
JJ Kasper 已提交
453 454 455
    await DocumentMiddleware(ctx)
  }

456 457 458 459 460 461 462 463 464
  const ampState = {
    ampFirst: pageConfig.amp === true,
    hasQuery: Boolean(query.amp),
    hybrid: pageConfig.amp === 'hybrid',
  }

  const reactLoadableModules: string[] = []

  const AppContainer = ({ children }: any) => (
465
    <RouterContext.Provider value={router}>
T
Tim Neutkens 已提交
466 467 468 469 470 471 472
      <AmpStateContext.Provider value={ampState}>
        <LoadableContext.Provider
          value={moduleName => reactLoadableModules.push(moduleName)}
        >
          {children}
        </LoadableContext.Provider>
      </AmpStateContext.Provider>
473
    </RouterContext.Provider>
474 475
  )

476
  try {
J
JJ Kasper 已提交
477 478 479 480 481 482
    props = await loadGetInitialProps(App, {
      AppTree: ctx.AppTree,
      Component,
      router,
      ctx,
    })
483 484 485 486 487

    if (isSSG) {
      props[STATIC_PROPS_ID] = true
    }

488
    let previewData: string | false | object | undefined
J
JJ Kasper 已提交
489

490
    if ((isSSG || getServerSideProps) && !isFallback) {
J
Joe Haddad 已提交
491 492 493
      // Reads of this are cached on the `req` object, so this should resolve
      // instantly. There's no need to pass this data down from a previous
      // invoke, where we'd have to consider server & serverless.
494 495 496 497
      previewData = tryGetPreviewData(req, res, previewProps)
    }

    if (isSSG && !isFallback) {
498
      const data = await getStaticProps!({
J
Joe Haddad 已提交
499
        ...(pageIsDynamic ? { params: query as ParsedUrlQuery } : undefined),
J
Joe Haddad 已提交
500 501 502
        ...(previewData !== false
          ? { preview: true, previewData: previewData }
          : undefined),
J
JJ Kasper 已提交
503
      })
J
JJ Kasper 已提交
504 505 506 507 508 509

      const invalidKeys = Object.keys(data).filter(
        key => key !== 'revalidate' && key !== 'props'
      )

      if (invalidKeys.length) {
510
        throw new Error(invalidKeysMsg('getStaticProps', invalidKeys))
J
JJ Kasper 已提交
511 512
      }

513 514 515 516 517 518 519 520 521 522
      if (
        (dev || isBuildTimeSSG) &&
        !isSerializableProps(pathname, 'getStaticProps', data.props)
      ) {
        // this fn should throw an error instead of ever returning `false`
        throw new Error(
          'invariant: getStaticProps did not return valid props. Please report this.'
        )
      }

J
JJ Kasper 已提交
523
      if (typeof data.revalidate === 'number') {
524
        if (!Number.isInteger(data.revalidate)) {
J
JJ Kasper 已提交
525
          throw new Error(
526
            `A page's revalidate option must be seconds expressed as a natural number. Mixed numbers, such as '${data.revalidate}', cannot be used.` +
527 528
              `\nTry changing the value to '${Math.ceil(
                data.revalidate
529
              )}' or using \`Math.ceil()\` if you're computing the value.`
530
          )
531
        } else if (data.revalidate <= 0) {
532
          throw new Error(
533 534 535
            `A page's revalidate option can not be less than or equal to zero. A revalidate option of zero means to revalidate after _every_ request, and implies stale data cannot be tolerated.` +
              `\n\nTo never revalidate, you can set revalidate to \`false\` (only ran once at build-time).` +
              `\nTo revalidate as soon as possible, you can set the value to \`1\`.`
J
JJ Kasper 已提交
536 537 538 539
          )
        } else if (data.revalidate > 31536000) {
          // if it's greater than a year for some reason error
          console.warn(
540 541
            `Warning: A page's revalidate option was set to more than a year. This may have been done in error.` +
              `\nTo only run getStaticProps at build-time and not revalidate at runtime, you can set \`revalidate\` to \`false\`!`
J
JJ Kasper 已提交
542 543
          )
        }
J
Joe Haddad 已提交
544 545
      } else if (data.revalidate === true) {
        // When enabled, revalidate after 1 second. This value is optimal for
546 547 548
        // the most up-to-date page possible, but without a 1-to-1
        // request-refresh ratio.
        data.revalidate = 1
J
Joe Haddad 已提交
549 550 551
      } else {
        // By default, we never revalidate.
        data.revalidate = false
J
JJ Kasper 已提交
552 553
      }

J
JJ Kasper 已提交
554 555
      props.pageProps = data.props
      // pass up revalidate and props for export
556
      // TODO: change this to a different passing mechanism
J
JJ Kasper 已提交
557
      ;(renderOpts as any).revalidate = data.revalidate
558
      ;(renderOpts as any).pageData = props
J
JJ Kasper 已提交
559
    }
560

561 562 563 564
    if (getServerSideProps) {
      props[SERVER_PROPS_ID] = true
    }

565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581
    if (getServerSideProps && !isFallback) {
      const data = await getServerSideProps({
        req,
        res,
        query,
        ...(pageIsDynamic ? { params: params as ParsedUrlQuery } : undefined),
        ...(previewData !== false
          ? { preview: true, previewData: previewData }
          : undefined),
      })

      const invalidKeys = Object.keys(data).filter(key => key !== 'props')

      if (invalidKeys.length) {
        throw new Error(invalidKeysMsg('getServerSideProps', invalidKeys))
      }

582 583 584 585 586 587 588 589 590 591
      if (
        (dev || isBuildTimeSSG) &&
        !isSerializableProps(pathname, 'getServerSideProps', data.props)
      ) {
        // this fn should throw an error instead of ever returning `false`
        throw new Error(
          'invariant: getServerSideProps did not return valid props. Please report this.'
        )
      }

592 593 594
      props.pageProps = data.props
      ;(renderOpts as any).pageData = props
    }
595
  } catch (err) {
596
    if (isDataReq || !dev || !err) throw err
597 598
    ctx.err = err
    renderOpts.err = err
599
    console.error(err)
600
  }
601

602
  if (
603
    !isSSG && // we only show this warning for legacy pages
604
    !getServerSideProps &&
605 606 607 608 609 610 611 612 613
    process.env.NODE_ENV !== 'production' &&
    Object.keys(props?.pageProps || {}).includes('url')
  ) {
    console.warn(
      `The prop \`url\` is a reserved prop in Next.js for legacy reasons and will be overridden on page ${pathname}\n` +
        `See more info here: https://err.sh/zeit/next.js/reserved-page-prop`
    )
  }

614
  // We only need to do this if we want to support calling
615
  // _app's getInitialProps for getServerSideProps if not this can be removed
616 617
  if (isDataReq) return props

618
  // We don't call getStaticProps or getServerSideProps while generating
619 620 621 622 623
  // the fallback so make sure to set pageProps to an empty object
  if (isFallback) {
    props.pageProps = {}
  }

624
  // the response might be finished on the getInitialProps call
625
  if (isResSent(res) && !isSSG) return null
626 627 628 629 630

  const devFiles = buildManifest.devFiles
  const files = [
    ...new Set([
      ...getPageFiles(buildManifest, '/_app'),
631
      ...getPageFiles(buildManifest, pathname),
632
    ]),
633
  ]
634
  const lowPriorityFiles = buildManifest.lowPriorityFiles
635
  const polyfillFiles = getPageFiles(buildManifest, '/_polyfills')
636

T
Tim Neutkens 已提交
637 638 639 640
  const renderElementToString = staticMarkup
    ? renderToStaticMarkup
    : renderToString

J
Joe Haddad 已提交
641
  const renderPageError = (): { html: string; head: any } | void => {
642
    if (ctx.err && ErrorDebug) {
J
Joe Haddad 已提交
643 644 645
      return render(
        renderElementToString,
        <ErrorDebug error={ctx.err} />,
646
        ampState
J
Joe Haddad 已提交
647
      )
648 649 650 651
    }

    if (dev && (props.router || props.Component)) {
      throw new Error(
J
Joe Haddad 已提交
652
        `'router' and 'Component' can not be returned in getInitialProps from _app.js https://err.sh/zeit/next.js/cant-override-next-props`
653 654 655 656
      )
    }
  }

T
Tim Neutkens 已提交
657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674
  let renderPage: RenderPage = (
    options: ComponentsEnhancer = {}
  ): { html: string; head: any } => {
    const renderError = renderPageError()
    if (renderError) return renderError

    const {
      App: EnhancedApp,
      Component: EnhancedComponent,
    } = enhanceComponents(options, App, Component)

    return render(
      renderElementToString,
      <AppContainer>
        <EnhancedApp Component={EnhancedComponent} router={router} {...props} />
      </AppContainer>,
      ampState
    )
T
Tim Neutkens 已提交
675
  }
676
  const documentCtx = { ...ctx, renderPage }
677 678 679 680
  const docProps: DocumentInitialProps = await loadGetInitialProps(
    Document,
    documentCtx
  )
K
Kévin Dunglas 已提交
681
  // the response might be finished on the getInitialProps call
682
  if (isResSent(res) && !isSSG) return null
683

684
  if (!docProps || typeof docProps.html !== 'string') {
J
Joe Haddad 已提交
685 686 687
    const message = `"${getDisplayName(
      Document
    )}.getInitialProps()" should resolve to an object with a "html" prop set with a valid html string`
688 689 690
    throw new Error(message)
  }

691 692 693 694
  const dynamicImportIdsSet = new Set<string>()
  const dynamicImports: ManifestItem[] = []

  for (const mod of reactLoadableModules) {
695
    const manifestItem: ManifestItem[] = reactLoadableManifest[mod]
696 697

    if (manifestItem) {
698
      manifestItem.forEach(item => {
699 700 701 702 703 704 705
        dynamicImports.push(item)
        dynamicImportIdsSet.add(item.id as string)
      })
    }
  }

  const dynamicImportsIds = [...dynamicImportIdsSet]
706 707 708
  const inAmpMode = isInAmpMode(ampState)
  const hybridAmp = ampState.hybrid

709
  // update renderOpts so export knows current state
710 711
  renderOpts.inAmpMode = inAmpMode
  renderOpts.hybridAmp = hybridAmp
712

J
JJ Kasper 已提交
713
  let html = renderDocument(Document, {
714
    ...renderOpts,
715
    dangerousAsPath: router.asPath,
716
    ampState,
717
    props,
718 719 720
    headTags: await headTags(documentCtx),
    bodyTags: await bodyTags(documentCtx),
    htmlProps: await htmlProps(documentCtx),
721
    isFallback,
722 723
    docProps,
    pathname,
724
    ampPath,
725
    query,
726 727
    inAmpMode,
    hybridAmp,
728 729
    dynamicImportsIds,
    dynamicImports,
730
    devFiles,
731 732
    files,
    lowPriorityFiles,
733
    polyfillFiles,
734 735
    gsp: !!getStaticProps ? true : undefined,
    gssp: !!getServerSideProps ? true : undefined,
736
  })
J
JJ Kasper 已提交
737

738
  if (inAmpMode && html) {
J
JJ Kasper 已提交
739 740 741 742 743 744 745
    // inject HTML to AMP_RENDER_TARGET to allow rendering
    // directly to body in AMP mode
    const ampRenderIndex = html.indexOf(AMP_RENDER_TARGET)
    html =
      html.substring(0, ampRenderIndex) +
      `<!-- __NEXT_DATA__ -->${docProps.html}` +
      html.substring(ampRenderIndex + AMP_RENDER_TARGET.length)
746
    html = await optimizeAmp(html)
747

748
    if (renderOpts.ampValidator) {
749 750
      await renderOpts.ampValidator(html, pathname)
    }
J
JJ Kasper 已提交
751
  }
J
JJ Kasper 已提交
752

753
  if (inAmpMode || hybridAmp) {
J
JJ Kasper 已提交
754 755 756
    // fix &amp being escaped for amphtml rel link
    html = html.replace(/&amp;amp=1/g, '&amp=1')
  }
J
JJ Kasper 已提交
757

J
JJ Kasper 已提交
758
  return html
759 760
}

761
function errorToJSON(err: Error): Error {
762 763 764 765
  const { name, message, stack } = err
  return { name, message, stack }
}

766 767
function serializeError(
  dev: boolean | undefined,
J
Joe Haddad 已提交
768
  err: Error
769
): Error & { statusCode?: number } {
770 771 772 773
  if (dev) {
    return errorToJSON(err)
  }

774 775 776 777 778
  return {
    name: 'Internal Server Error.',
    message: '500 - Internal Server Error.',
    statusCode: 500,
  }
779
}