render.tsx 30.4 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'
5
import { warn } from '../../build/output/log'
6
import { UnwrapPromise } from '../../lib/coalesced-function'
J
Joe Haddad 已提交
7
import {
8 9 10
  GSP_NO_RETURNED_VALUE,
  GSSP_COMPONENT_MEMBER_ERROR,
  GSSP_NO_RETURNED_VALUE,
J
Joe Haddad 已提交
11 12 13 14
  PAGES_404_GET_INITIAL_PROPS_ERROR,
  SERVER_PROPS_GET_INIT_PROPS_CONFLICT,
  SERVER_PROPS_SSG_CONFLICT,
  SSG_GET_INITIAL_PROPS_CONFLICT,
J
JJ Kasper 已提交
15
  UNSTABLE_REVALIDATE_RENAME_ERROR,
J
Joe Haddad 已提交
16
} from '../../lib/constants'
17
import { isSerializableProps } from '../../lib/is-serializable-props'
18
import { GetServerSideProps, GetStaticProps } from '../../types'
J
Joe Haddad 已提交
19 20
import { isInAmpMode } from '../lib/amp'
import { AmpStateContext } from '../lib/amp-context'
21 22
import {
  AMP_RENDER_TARGET,
23
  PERMANENT_REDIRECT_STATUS,
24
  SERVER_PROPS_ID,
25
  STATIC_PROPS_ID,
26
  TEMPORARY_REDIRECT_STATUS,
27
} from '../lib/constants'
28
import { defaultHead } from '../lib/head'
29
import { HeadManagerContext } from '../lib/head-manager-context'
30
import Loadable from '../lib/loadable'
31
import { LoadableContext } from '../lib/loadable-context'
J
Joe Haddad 已提交
32
import mitt, { MittEmitter } from '../lib/mitt'
33
import postProcess from '../lib/post-process'
T
Tim Neutkens 已提交
34
import { RouterContext } from '../lib/router-context'
J
Joe Haddad 已提交
35
import { NextRouter } from '../lib/router/router'
J
JJ Kasper 已提交
36
import { isDynamicRoute } from '../lib/router/utils/is-dynamic'
37
import {
J
Joe Haddad 已提交
38 39 40
  AppType,
  ComponentsEnhancer,
  DocumentInitialProps,
41
  DocumentProps,
J
Joe Haddad 已提交
42 43 44 45 46 47 48 49
  DocumentType,
  getDisplayName,
  isResSent,
  loadGetInitialProps,
  NextComponentType,
  RenderPage,
} from '../lib/utils'
import { tryGetPreviewData, __ApiPreviewProps } from './api-utils'
50 51
import { denormalizePagePath } from './denormalize-page-path'
import { FontManifest, getFontDefinitionFromManifest } from './font-utils'
52
import { LoadComponentsReturnType, ManifestItem } from './load-components'
53
import { normalizePagePath } from './normalize-page-path'
J
Joe Haddad 已提交
54
import optimizeAmp from './optimize-amp'
55

56
function noRouter() {
J
Joe Haddad 已提交
57
  const message =
58
    'No router instance found. you should only use "next/router" inside the client side of your app. https://err.sh/vercel/next.js/no-router-instance'
59 60 61
  throw new Error(message)
}

62
class ServerRouter implements NextRouter {
63 64
  route: string
  pathname: string
65
  query: ParsedUrlQuery
66
  asPath: string
67
  basePath: string
68
  events: any
69
  isFallback: boolean
70 71
  locale?: string
  locales?: string[]
72
  defaultLocale?: string
73 74 75
  // 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()

76 77 78 79
  constructor(
    pathname: string,
    query: ParsedUrlQuery,
    as: string,
80
    { isFallback }: { isFallback: boolean },
81 82
    basePath: string,
    locale?: string,
83 84
    locales?: string[],
    defaultLocale?: string
85
  ) {
T
Tim Neutkens 已提交
86
    this.route = pathname.replace(/\/$/, '') || '/'
87 88 89
    this.pathname = pathname
    this.query = query
    this.asPath = as
90
    this.isFallback = isFallback
91
    this.basePath = basePath
92 93
    this.locale = locale
    this.locales = locales
94
    this.defaultLocale = defaultLocale
95
  }
96
  push(): any {
97 98
    noRouter()
  }
99
  replace(): any {
100 101 102 103 104 105 106 107
    noRouter()
  }
  reload() {
    noRouter()
  }
  back() {
    noRouter()
  }
108
  prefetch(): any {
109 110 111 112 113 114 115
    noRouter()
  }
  beforePopState() {
    noRouter()
  }
}

116 117
function enhanceComponents(
  options: ComponentsEnhancer,
118
  App: AppType,
J
Joe Haddad 已提交
119
  Component: NextComponentType
120
): {
J
Joe Haddad 已提交
121 122
  App: AppType
  Component: NextComponentType
123 124
} {
  // For backwards compatibility
125
  if (typeof options === 'function') {
126
    return {
127 128
      App,
      Component: options(Component),
129 130 131 132 133
    }
  }

  return {
    App: options.enhanceApp ? options.enhanceApp(App) : App,
134 135 136
    Component: options.enhanceComponent
      ? options.enhanceComponent(Component)
      : Component,
137 138 139
  }
}

140
export type RenderOptsPartial = {
141
  buildId: string
142
  canonicalBase: string
143 144 145
  runtimeConfig?: { [key: string]: any }
  assetPrefix?: string
  err?: Error | null
146
  autoExport?: boolean
147 148
  nextExport?: boolean
  dev?: boolean
J
Joe Haddad 已提交
149
  ampMode?: any
150 151 152
  ampPath?: string
  inAmpMode?: boolean
  hybridAmp?: boolean
J
Joe Haddad 已提交
153 154
  ErrorDebug?: React.ComponentType<{ error: Error }>
  ampValidator?: (html: string, pathname: string) => Promise<void>
155 156
  ampSkipValidation?: boolean
  ampOptimizerConfig?: { [key: string]: any }
157 158
  isDataReq?: boolean
  params?: ParsedUrlQuery
J
Joe Haddad 已提交
159
  previewProps: __ApiPreviewProps
160
  basePath: string
161
  unstable_runtimeJS?: false
P
Prateek Bhatnagar 已提交
162 163
  optimizeFonts: boolean
  fontManifest?: FontManifest
164
  optimizeImages: boolean
165
  devOnlyCacheBusterQueryString?: string
166
  resolvedUrl?: string
167
  resolvedAsPath?: string
168 169
  locale?: string
  locales?: string[]
170
  defaultLocale?: string
171 172
}

173 174
export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial

175
function renderDocument(
176
  Document: DocumentType,
177
  {
178
    buildManifest,
179
    docComponentsRendered,
180 181 182 183 184
    props,
    docProps,
    pathname,
    query,
    buildId,
185
    canonicalBase,
186 187 188
    assetPrefix,
    runtimeConfig,
    nextExport,
189
    autoExport,
190
    isFallback,
191
    dynamicImportsIds,
192
    dangerousAsPath,
193 194
    err,
    dev,
195
    ampPath,
196 197 198
    ampState,
    inAmpMode,
    hybridAmp,
199
    dynamicImports,
200
    headTags,
201 202
    gsp,
    gssp,
203
    customServer,
204 205
    gip,
    appGip,
206
    unstable_runtimeJS,
207
    devOnlyCacheBusterQueryString,
208 209
    locale,
    locales,
210
    defaultLocale,
211 212
  }: RenderOpts & {
    props: any
213
    docComponentsRendered: DocumentProps['docComponentsRendered']
214
    docProps: DocumentInitialProps
215 216
    pathname: string
    query: ParsedUrlQuery
217
    dangerousAsPath: string
218
    ampState: any
J
Joe Haddad 已提交
219
    ampPath: string
220 221
    inAmpMode: boolean
    hybridAmp: boolean
222 223
    dynamicImportsIds: string[]
    dynamicImports: ManifestItem[]
224
    headTags: any
225
    isFallback?: boolean
226 227
    gsp?: boolean
    gssp?: boolean
228
    customServer?: boolean
229 230
    gip?: boolean
    appGip?: boolean
231
    devOnlyCacheBusterQueryString: string
J
Joe Haddad 已提交
232
  }
233 234 235 236
): string {
  return (
    '<!DOCTYPE html>' +
    renderToStaticMarkup(
237
      <AmpStateContext.Provider value={ampState}>
G
Gerald Monaco 已提交
238 239
        {Document.renderDocument(Document, {
          __NEXT_DATA__: {
240 241 242 243 244 245 246
            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`
247
            autoExport, // If this is an auto exported page
248
            isFallback,
J
Joe Haddad 已提交
249 250
            dynamicIds:
              dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds,
251
            err: err ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML
252 253
            gsp, // whether the page is getStaticProps
            gssp, // whether the page is getServerSideProps
254
            customServer, // whether the user is using a custom server
255 256
            gip, // whether the page has getInitialProps
            appGip, // whether the _app has getInitialProps
257 258
            locale,
            locales,
259
            defaultLocale,
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
            head: React.Children.toArray(docProps.head || [])
              .map((elem) => {
                const { children } = elem?.props
                return [
                  elem?.type,
                  {
                    ...elem?.props,
                    children: children
                      ? typeof children === 'string'
                        ? children
                        : children.join('')
                      : undefined,
                  },
                ]
              })
              .filter(Boolean) as any,
G
Gerald Monaco 已提交
276
          },
277
          buildManifest,
278
          docComponentsRendered,
G
Gerald Monaco 已提交
279 280 281 282 283 284 285 286
          dangerousAsPath,
          canonicalBase,
          ampPath,
          inAmpMode,
          isDevelopment: !!dev,
          hybridAmp,
          dynamicImports,
          assetPrefix,
287
          headTags,
288
          unstable_runtimeJS,
289
          devOnlyCacheBusterQueryString,
290
          locale,
G
Gerald Monaco 已提交
291 292
          ...docProps,
        })}
293
      </AmpStateContext.Provider>
294
    )
295 296 297
  )
}

298 299 300 301
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: '...' } }` +
302 303
    `\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.` +
    `\nRead more: https://err.sh/next.js/invalid-getstaticprops-value`
304 305 306
  )
}

307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
type Redirect = {
  permanent: boolean
  destination: string
}

function checkRedirectValues(redirect: Redirect, req: IncomingMessage) {
  const { destination, permanent } = redirect
  let invalidPermanent = typeof permanent !== 'boolean'
  let invalidDestination = typeof destination !== 'string'

  if (invalidPermanent || invalidDestination) {
    throw new Error(
      `Invalid redirect object returned from getStaticProps for ${req.url}\n` +
        `Expected${
          invalidPermanent
            ? ` \`permanent\` to be boolean but received ${typeof permanent}`
            : ''
        }${invalidPermanent && invalidDestination ? ' and' : ''}${
          invalidDestination
            ? ` \`destinatino\` to be string but received ${typeof destination}`
            : ''
        }\n` +
        `See more info here: https://err.sh/vercel/next.js/invalid-redirect-gssp`
    )
  }
}

function handleRedirect(res: ServerResponse, redirect: Redirect) {
  const statusCode = redirect.permanent
    ? PERMANENT_REDIRECT_STATUS
    : TEMPORARY_REDIRECT_STATUS

  if (redirect.permanent) {
    res.setHeader('Refresh', `0;url=${redirect.destination}`)
  }
  res.statusCode = statusCode
  res.setHeader('Location', redirect.destination)
  res.end()
}

347 348 349 350 351
export async function renderToHTML(
  req: IncomingMessage,
  res: ServerResponse,
  pathname: string,
  query: ParsedUrlQuery,
J
Joe Haddad 已提交
352
  renderOpts: RenderOpts
353
): Promise<string | null> {
354 355 356 357 358 359 360
  // In dev we invalidate the cache by appending a timestamp to the resource URL.
  // This is a workaround to fix https://github.com/vercel/next.js/issues/5860
  // TODO: remove this workaround when https://bugs.webkit.org/show_bug.cgi?id=187726 is fixed.
  renderOpts.devOnlyCacheBusterQueryString = renderOpts.dev
    ? renderOpts.devOnlyCacheBusterQueryString || `?ts=${Date.now()}`
    : ''

361 362 363
  const {
    err,
    dev = false,
364
    ampPath = '',
365 366
    App,
    Document,
367
    pageConfig = {},
368 369
    Component,
    buildManifest,
P
Prateek Bhatnagar 已提交
370
    fontManifest,
371
    reactLoadableManifest,
372
    ErrorDebug,
373 374 375
    getStaticProps,
    getStaticPaths,
    getServerSideProps,
376 377
    isDataReq,
    params,
J
Joe Haddad 已提交
378
    previewProps,
379
    basePath,
380
    devOnlyCacheBusterQueryString,
381
  } = renderOpts
382

P
Prateek Bhatnagar 已提交
383 384 385 386 387 388 389
  const getFontDefinition = (url: string): string => {
    if (fontManifest) {
      return getFontDefinitionFromManifest(url, fontManifest)
    }
    return ''
  }

390 391 392 393
  const callMiddleware = async (method: string, args: any[], props = false) => {
    let results: any = props ? {} : []

    if ((Document as any)[`${method}Middleware`]) {
394 395 396 397
      let middlewareFunc = await (Document as any)[`${method}Middleware`]
      middlewareFunc = middlewareFunc.default || middlewareFunc

      const curResults = await middlewareFunc(...args)
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413
      if (props) {
        for (const result of curResults) {
          results = {
            ...results,
            ...result,
          }
        }
      } else {
        results = curResults
      }
    }
    return results
  }

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

414 415
  const isFallback = !!query.__nextFallback
  delete query.__nextFallback
416
  delete query.__nextLocale
417

418
  const isSSG = !!getStaticProps
419
  const isBuildTimeSSG = isSSG && renderOpts.nextExport
420 421 422
  const defaultAppGetInitialProps =
    App.getInitialProps === (App as any).origGetInitialProps

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

425 426
  const pageIsDynamic = isDynamicRoute(pathname)

J
JJ Kasper 已提交
427
  const isAutoExport =
428 429
    !hasPageGetInitialProps &&
    defaultAppGetInitialProps &&
430
    !isSSG &&
431
    !getServerSideProps
J
JJ Kasper 已提交
432

433 434 435 436 437 438 439 440 441 442 443 444
  for (const methodName of [
    'getStaticProps',
    'getServerSideProps',
    'getStaticPaths',
  ]) {
    if ((Component as any)[methodName]) {
      throw new Error(
        `page ${pathname} ${methodName} ${GSSP_COMPONENT_MEMBER_ERROR}`
      )
    }
  }

445
  if (hasPageGetInitialProps && isSSG) {
446
    throw new Error(SSG_GET_INITIAL_PROPS_CONFLICT + ` ${pathname}`)
J
JJ Kasper 已提交
447
  }
448

449
  if (hasPageGetInitialProps && getServerSideProps) {
450 451 452
    throw new Error(SERVER_PROPS_GET_INIT_PROPS_CONFLICT + ` ${pathname}`)
  }

453
  if (getServerSideProps && isSSG) {
454 455 456
    throw new Error(SERVER_PROPS_SSG_CONFLICT + ` ${pathname}`)
  }

457
  if (!!getStaticPaths && !isSSG) {
458
    throw new Error(
459
      `getStaticPaths was added without a getStaticProps in ${pathname}. Without getStaticProps, getStaticPaths does nothing`
460 461 462
    )
  }

463
  if (isSSG && pageIsDynamic && !getStaticPaths) {
464
    throw new Error(
465
      `getStaticPaths is required for dynamic SSG pages and is missing for '${pathname}'.` +
466 467 468 469
        `\nRead more: https://err.sh/next.js/invalid-getstaticpaths-value`
    )
  }

470 471 472
  if (dev) {
    const { isValidElementType } = require('react-is')
    if (!isValidElementType(Component)) {
473
      throw new Error(
J
Joe Haddad 已提交
474
        `The default export is not a React Component in page: "${pathname}"`
475
      )
476 477
    }

478
    if (!isValidElementType(App)) {
479
      throw new Error(
J
Joe Haddad 已提交
480
        `The default export is not a React Component in page: "/_app"`
481
      )
482 483 484
    }

    if (!isValidElementType(Document)) {
485
      throw new Error(
J
Joe Haddad 已提交
486
        `The default export is not a React Component in page: "/_document"`
487
      )
488
    }
J
JJ Kasper 已提交
489

490
    if (isAutoExport || isFallback) {
491 492
      // remove query values except ones that will be set during export
      query = {
493 494 495 496 497
        ...(query.amp
          ? {
              amp: query.amp,
            }
          : {}),
J
JJ Kasper 已提交
498
      }
499
      req.url = pathname
500
      renderOpts.resolvedAsPath = pathname
501
      renderOpts.nextExport = true
J
JJ Kasper 已提交
502
    }
503

504
    if (pathname === '/404' && (hasPageGetInitialProps || getServerSideProps)) {
505 506
      throw new Error(PAGES_404_GET_INITIAL_PROPS_ERROR)
    }
507
  }
508
  if (isAutoExport) renderOpts.autoExport = true
509
  if (isSSG) renderOpts.nextExport = false
510

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

513
  // url will always be set
514
  const asPath: string = renderOpts.resolvedAsPath || (req.url as string)
515 516 517 518 519 520 521
  const router = new ServerRouter(
    pathname,
    query,
    asPath,
    {
      isFallback: isFallback,
    },
522 523
    basePath,
    renderOpts.locale,
524 525
    renderOpts.locales,
    renderOpts.defaultLocale
526
  )
527 528
  const ctx = {
    err,
J
JJ Kasper 已提交
529 530
    req: isAutoExport ? undefined : req,
    res: isAutoExport ? undefined : res,
531 532 533
    pathname,
    query,
    asPath,
534 535 536 537 538 539 540
    AppTree: (props: any) => {
      return (
        <AppContainer>
          <App {...props} Component={Component} router={router} />
        </AppContainer>
      )
    },
541
  }
542
  let props: any
543

544 545 546 547 548 549
  const ampState = {
    ampFirst: pageConfig.amp === true,
    hasQuery: Boolean(query.amp),
    hybrid: pageConfig.amp === 'hybrid',
  }

550 551
  const inAmpMode = isInAmpMode(ampState)

552 553
  const reactLoadableModules: string[] = []

554 555
  let head: JSX.Element[] = defaultHead(inAmpMode)

556
  const AppContainer = ({ children }: any) => (
557
    <RouterContext.Provider value={router}>
T
Tim Neutkens 已提交
558
      <AmpStateContext.Provider value={ampState}>
559 560 561 562 563 564 565
        <HeadManagerContext.Provider
          value={{
            updateHead: (state) => {
              head = state
            },
            mountedInstances: new Set(),
          }}
T
Tim Neutkens 已提交
566
        >
567 568 569 570 571 572
          <LoadableContext.Provider
            value={(moduleName) => reactLoadableModules.push(moduleName)}
          >
            {children}
          </LoadableContext.Provider>
        </HeadManagerContext.Provider>
T
Tim Neutkens 已提交
573
      </AmpStateContext.Provider>
574
    </RouterContext.Provider>
575 576
  )

577
  try {
J
JJ Kasper 已提交
578 579 580 581 582 583
    props = await loadGetInitialProps(App, {
      AppTree: ctx.AppTree,
      Component,
      router,
      ctx,
    })
584 585 586 587 588

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

589
    let previewData: string | false | object | undefined
J
JJ Kasper 已提交
590

591
    if ((isSSG || getServerSideProps) && !isFallback) {
J
Joe Haddad 已提交
592 593 594
      // 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.
595 596 597 598
      previewData = tryGetPreviewData(req, res, previewProps)
    }

    if (isSSG && !isFallback) {
599 600 601 602 603 604 605 606
      let data: UnwrapPromise<ReturnType<GetStaticProps>>

      try {
        data = await getStaticProps!({
          ...(pageIsDynamic ? { params: query as ParsedUrlQuery } : undefined),
          ...(previewData !== false
            ? { preview: true, previewData: previewData }
            : undefined),
607 608
          locales: renderOpts.locales,
          locale: renderOpts.locale,
609
        })
610
      } catch (staticPropsError) {
611 612
        // remove not found error code to prevent triggering legacy
        // 404 rendering
613 614
        if (staticPropsError.code === 'ENOENT') {
          delete staticPropsError.code
615
        }
616
        throw staticPropsError
617
      }
J
JJ Kasper 已提交
618

619 620 621 622
      if (data == null) {
        throw new Error(GSP_NO_RETURNED_VALUE)
      }

J
JJ Kasper 已提交
623
      const invalidKeys = Object.keys(data).filter(
624
        (key) =>
625 626 627 628
          key !== 'revalidate' &&
          key !== 'props' &&
          key !== 'unstable_redirect' &&
          key !== 'unstable_notFound'
J
JJ Kasper 已提交
629 630
      )

T
Tim Neutkens 已提交
631
      if (invalidKeys.includes('unstable_revalidate')) {
J
JJ Kasper 已提交
632 633 634
        throw new Error(UNSTABLE_REVALIDATE_RENAME_ERROR)
      }

J
JJ Kasper 已提交
635
      if (invalidKeys.length) {
636
        throw new Error(invalidKeysMsg('getStaticProps', invalidKeys))
J
JJ Kasper 已提交
637 638
      }

639
      if (data.unstable_notFound) {
640 641 642 643 644 645
        if (pathname === '/404') {
          throw new Error(
            `The /404 page can not return unstable_notFound in "getStaticProps", please remove it to continue!`
          )
        }

646
        ;(renderOpts as any).isNotFound = true
647 648 649 650
        ;(renderOpts as any).revalidate = false
        return null
      }

651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673
      if (
        data.unstable_redirect &&
        typeof data.unstable_redirect === 'object'
      ) {
        checkRedirectValues(data.unstable_redirect, req)

        if (isBuildTimeSSG) {
          throw new Error(
            `\`redirect\` can not be returned from getStaticProps during prerendering (${req.url})\n` +
              `See more info here: https://err.sh/next.js/gsp-redirect-during-prerender`
          )
        }

        if (isDataReq) {
          data.props = {
            __N_REDIRECT: data.unstable_redirect.destination,
          }
        } else {
          handleRedirect(res, data.unstable_redirect)
          return null
        }
      }

674 675 676 677 678 679 680 681 682 683
      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.'
        )
      }

T
Tim Neutkens 已提交
684 685
      if (typeof data.revalidate === 'number') {
        if (!Number.isInteger(data.revalidate)) {
J
JJ Kasper 已提交
686
          throw new Error(
T
Tim Neutkens 已提交
687
            `A page's revalidate option must be seconds expressed as a natural number. Mixed numbers, such as '${data.revalidate}', cannot be used.` +
688
              `\nTry changing the value to '${Math.ceil(
T
Tim Neutkens 已提交
689
                data.revalidate
690
              )}' or using \`Math.ceil()\` if you're computing the value.`
691
          )
T
Tim Neutkens 已提交
692
        } else if (data.revalidate <= 0) {
693
          throw new Error(
694 695 696
            `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 已提交
697
          )
T
Tim Neutkens 已提交
698
        } else if (data.revalidate > 31536000) {
J
JJ Kasper 已提交
699 700
          // if it's greater than a year for some reason error
          console.warn(
701 702
            `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 已提交
703 704
          )
        }
T
Tim Neutkens 已提交
705
      } else if (data.revalidate === true) {
J
Joe Haddad 已提交
706
        // When enabled, revalidate after 1 second. This value is optimal for
707 708
        // the most up-to-date page possible, but without a 1-to-1
        // request-refresh ratio.
T
Tim Neutkens 已提交
709
        data.revalidate = 1
J
Joe Haddad 已提交
710 711
      } else {
        // By default, we never revalidate.
T
Tim Neutkens 已提交
712
        data.revalidate = false
J
JJ Kasper 已提交
713 714
      }

715
      props.pageProps = Object.assign({}, props.pageProps, data.props)
J
JJ Kasper 已提交
716
      // pass up revalidate and props for export
717
      // TODO: change this to a different passing mechanism
T
Tim Neutkens 已提交
718
      ;(renderOpts as any).revalidate = data.revalidate
719
      ;(renderOpts as any).pageData = props
J
JJ Kasper 已提交
720
    }
721

722 723 724 725
    if (getServerSideProps) {
      props[SERVER_PROPS_ID] = true
    }

726
    if (getServerSideProps && !isFallback) {
727 728 729 730 731 732 733
      let data: UnwrapPromise<ReturnType<GetServerSideProps>>

      try {
        data = await getServerSideProps({
          req,
          res,
          query,
734
          resolvedUrl: renderOpts.resolvedUrl as string,
735 736 737 738
          ...(pageIsDynamic ? { params: params as ParsedUrlQuery } : undefined),
          ...(previewData !== false
            ? { preview: true, previewData: previewData }
            : undefined),
739 740
          locales: renderOpts.locales,
          locale: renderOpts.locale,
741
        })
742
      } catch (serverSidePropsError) {
743 744
        // remove not found error code to prevent triggering legacy
        // 404 rendering
745 746
        if (serverSidePropsError.code === 'ENOENT') {
          delete serverSidePropsError.code
747
        }
748
        throw serverSidePropsError
749
      }
750

751 752 753 754
      if (data == null) {
        throw new Error(GSSP_NO_RETURNED_VALUE)
      }

755
      const invalidKeys = Object.keys(data).filter(
756 757 758 759
        (key) =>
          key !== 'props' &&
          key !== 'unstable_redirect' &&
          key !== 'unstable_notFound'
760
      )
761 762 763 764 765

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

766 767 768 769 770 771 772 773 774 775 776
      if ('unstable_notFound' in data) {
        if (pathname === '/404') {
          throw new Error(
            `The /404 page can not return unstable_notFound in "getStaticProps", please remove it to continue!`
          )
        }

        ;(renderOpts as any).isNotFound = true
        return null
      }

777
      if (
778
        'unstable_redirect' in data &&
779 780 781 782 783
        typeof data.unstable_redirect === 'object'
      ) {
        checkRedirectValues(data.unstable_redirect, req)

        if (isDataReq) {
784
          ;(data as any).props = {
785 786 787 788 789 790 791 792
            __N_REDIRECT: data.unstable_redirect.destination,
          }
        } else {
          handleRedirect(res, data.unstable_redirect)
          return null
        }
      }

793 794
      if (
        (dev || isBuildTimeSSG) &&
795 796 797 798 799
        !isSerializableProps(
          pathname,
          'getServerSideProps',
          (data as any).props
        )
800 801 802 803 804 805 806
      ) {
        // this fn should throw an error instead of ever returning `false`
        throw new Error(
          'invariant: getServerSideProps did not return valid props. Please report this.'
        )
      }

807
      props.pageProps = Object.assign({}, props.pageProps, (data as any).props)
808 809
      ;(renderOpts as any).pageData = props
    }
810 811 812 813 814
  } catch (dataFetchError) {
    if (isDataReq || !dev || !dataFetchError) throw dataFetchError
    ctx.err = dataFetchError
    renderOpts.err = dataFetchError
    console.error(dataFetchError)
815
  }
816

817
  if (
818
    !isSSG && // we only show this warning for legacy pages
819
    !getServerSideProps &&
820 821 822 823 824
    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` +
825
        `See more info here: https://err.sh/vercel/next.js/reserved-page-prop`
826 827 828
    )
  }

829
  // We only need to do this if we want to support calling
830
  // _app's getInitialProps for getServerSideProps if not this can be removed
831
  if (isDataReq && !isSSG) return props
832

833
  // We don't call getStaticProps or getServerSideProps while generating
834 835 836 837 838
  // the fallback so make sure to set pageProps to an empty object
  if (isFallback) {
    props.pageProps = {}
  }

839
  // the response might be finished on the getInitialProps call
840
  if (isResSent(res) && !isSSG) return null
841

842 843 844 845
  // we preload the buildManifest for auto-export dynamic pages
  // to speed up hydrating query values
  let filteredBuildManifest = buildManifest
  if (isAutoExport && pageIsDynamic) {
846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865
    const page = denormalizePagePath(normalizePagePath(pathname))
    // This code would be much cleaner using `immer` and directly pushing into
    // the result from `getPageFiles`, we could maybe consider that in the
    // future.
    if (page in filteredBuildManifest.pages) {
      filteredBuildManifest = {
        ...filteredBuildManifest,
        pages: {
          ...filteredBuildManifest.pages,
          [page]: [
            ...filteredBuildManifest.pages[page],
            ...filteredBuildManifest.lowPriorityFiles.filter((f) =>
              f.includes('_buildManifest')
            ),
          ],
        },
        lowPriorityFiles: filteredBuildManifest.lowPriorityFiles.filter(
          (f) => !f.includes('_buildManifest')
        ),
      }
866 867 868
    }
  }

869 870 871
  const renderPage: RenderPage = (
    options: ComponentsEnhancer = {}
  ): { html: string; head: any } => {
872
    if (ctx.err && ErrorDebug) {
873
      return { html: renderToString(<ErrorDebug error={ctx.err} />), head }
874 875 876 877
    }

    if (dev && (props.router || props.Component)) {
      throw new Error(
878
        `'router' and 'Component' can not be returned in getInitialProps from _app.js https://err.sh/vercel/next.js/cant-override-next-props`
879 880
      )
    }
T
Tim Neutkens 已提交
881 882 883 884 885 886

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

887
    const html = renderToString(
T
Tim Neutkens 已提交
888 889
      <AppContainer>
        <EnhancedApp Component={EnhancedComponent} router={router} {...props} />
890
      </AppContainer>
T
Tim Neutkens 已提交
891
    )
892 893

    return { html, head }
T
Tim Neutkens 已提交
894
  }
895
  const documentCtx = { ...ctx, renderPage }
896 897 898 899
  const docProps: DocumentInitialProps = await loadGetInitialProps(
    Document,
    documentCtx
  )
K
Kévin Dunglas 已提交
900
  // the response might be finished on the getInitialProps call
901
  if (isResSent(res) && !isSSG) return null
902

903
  if (!docProps || typeof docProps.html !== 'string') {
J
Joe Haddad 已提交
904 905 906
    const message = `"${getDisplayName(
      Document
    )}.getInitialProps()" should resolve to an object with a "html" prop set with a valid html string`
907 908 909
    throw new Error(message)
  }

910 911 912 913
  const dynamicImportIdsSet = new Set<string>()
  const dynamicImports: ManifestItem[] = []

  for (const mod of reactLoadableModules) {
914
    const manifestItem: ManifestItem[] = reactLoadableManifest[mod]
915 916

    if (manifestItem) {
J
Joe Haddad 已提交
917
      manifestItem.forEach((item) => {
918 919 920 921 922 923 924
        dynamicImports.push(item)
        dynamicImportIdsSet.add(item.id as string)
      })
    }
  }

  const dynamicImportsIds = [...dynamicImportIdsSet]
925 926
  const hybridAmp = ampState.hybrid

927
  // update renderOpts so export knows current state
928 929
  renderOpts.inAmpMode = inAmpMode
  renderOpts.hybridAmp = hybridAmp
930

931 932
  const docComponentsRendered: DocumentProps['docComponentsRendered'] = {}

J
JJ Kasper 已提交
933
  let html = renderDocument(Document, {
934
    ...renderOpts,
J
JJ Kasper 已提交
935 936 937 938
    canonicalBase:
      !renderOpts.ampPath && (req as any).__nextStrippedLocale
        ? `${renderOpts.canonicalBase || ''}/${renderOpts.locale}`
        : renderOpts.canonicalBase,
939
    docComponentsRendered,
940
    buildManifest: filteredBuildManifest,
941 942 943 944 945
    // Only enabled in production as development mode has features relying on HMR (style injection for example)
    unstable_runtimeJS:
      process.env.NODE_ENV === 'production'
        ? pageConfig.unstable_runtimeJS
        : undefined,
946
    dangerousAsPath: router.asPath,
947
    ampState,
948
    props,
949
    headTags: await headTags(documentCtx),
950
    isFallback,
951 952
    docProps,
    pathname,
953
    ampPath,
954
    query,
955 956
    inAmpMode,
    hybridAmp,
957 958
    dynamicImportsIds,
    dynamicImports,
959 960
    gsp: !!getStaticProps ? true : undefined,
    gssp: !!getServerSideProps ? true : undefined,
961 962
    gip: hasPageGetInitialProps ? true : undefined,
    appGip: !defaultAppGetInitialProps ? true : undefined,
963
    devOnlyCacheBusterQueryString,
964
  })
J
JJ Kasper 已提交
965

966 967 968 969 970 971 972 973 974 975 976 977
  if (process.env.NODE_ENV !== 'production') {
    const nonRenderedComponents = []
    const expectedDocComponents = ['Main', 'Head', 'NextScript', 'Html']

    for (const comp of expectedDocComponents) {
      if (!(docComponentsRendered as any)[comp]) {
        nonRenderedComponents.push(comp)
      }
    }
    const plural = nonRenderedComponents.length !== 1 ? 's' : ''

    if (nonRenderedComponents.length) {
978 979 980 981 982 983 984
      const missingComponentList = nonRenderedComponents
        .map((e) => `<${e} />`)
        .join(', ')
      warn(
        `Your custom Document (pages/_document) did not render all the required subcomponent${plural}.\n` +
          `Missing component${plural}: ${missingComponentList}\n` +
          'Read how to fix here: https://err.sh/next.js/missing-document-component'
985 986 987 988
      )
    }
  }

989
  if (inAmpMode && html) {
J
JJ Kasper 已提交
990 991 992 993 994 995 996
    // 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)
997
    html = await optimizeAmp(html, renderOpts.ampOptimizerConfig)
998

999
    if (!renderOpts.ampSkipValidation && renderOpts.ampValidator) {
1000 1001
      await renderOpts.ampValidator(html, pathname)
    }
J
JJ Kasper 已提交
1002
  }
J
JJ Kasper 已提交
1003

P
Prateek Bhatnagar 已提交
1004 1005 1006 1007 1008 1009 1010
  html = await postProcess(
    html,
    {
      getFontDefinition,
    },
    {
      optimizeFonts: renderOpts.optimizeFonts,
1011
      optimizeImages: renderOpts.optimizeImages,
P
Prateek Bhatnagar 已提交
1012 1013 1014
    }
  )

1015
  if (inAmpMode || hybridAmp) {
J
JJ Kasper 已提交
1016 1017 1018
    // fix &amp being escaped for amphtml rel link
    html = html.replace(/&amp;amp=1/g, '&amp=1')
  }
J
JJ Kasper 已提交
1019

J
JJ Kasper 已提交
1020
  return html
1021 1022
}

1023
function errorToJSON(err: Error): Error {
1024 1025 1026 1027
  const { name, message, stack } = err
  return { name, message, stack }
}

1028 1029
function serializeError(
  dev: boolean | undefined,
J
Joe Haddad 已提交
1030
  err: Error
1031
): Error & { statusCode?: number } {
1032 1033 1034 1035
  if (dev) {
    return errorToJSON(err)
  }

1036 1037 1038 1039 1040
  return {
    name: 'Internal Server Error.',
    message: '500 - Internal Server Error.',
    statusCode: 500,
  }
1041
}