render.tsx 15.1 KB
Newer Older
1
import { IncomingMessage, ServerResponse } from 'http'
2 3 4
import { ParsedUrlQuery } from 'querystring'
import React from 'react'
import { renderToString, renderToStaticMarkup } from 'react-dom/server'
5
import { NextRouter } from '../lib/router/router'
J
Joe Haddad 已提交
6 7 8 9 10 11 12 13 14 15 16 17 18
import mitt, { MittEmitter } from '../lib/mitt'
import {
  loadGetInitialProps,
  isResSent,
  getDisplayName,
  ComponentsEnhancer,
  RenderPage,
  DocumentInitialProps,
  NextComponentType,
  DocumentType,
  AppType,
  NextPageContext,
} from '../lib/utils'
19
import Head, { defaultHead } from '../lib/head'
T
Tim Neutkens 已提交
20
// @ts-ignore types will be added later as it's an internal module
21
import Loadable from '../lib/loadable'
T
Tim Neutkens 已提交
22
import { DataManagerContext } from '../lib/data-manager-context'
23
import { LoadableContext } from '../lib/loadable-context'
T
Tim Neutkens 已提交
24
import { RouterContext } from '../lib/router-context'
25
import { DataManager } from '../lib/data-manager'
26
import { getPageFiles, BuildManifest } from './get-page-files'
27
import { AmpStateContext } from '../lib/amp-context'
J
JJ Kasper 已提交
28
import optimizeAmp from './optimize-amp'
29
import { isInAmpMode } from '../lib/amp'
30 31
// Uses a module path because of the compiled output directory location
import { PageConfig } from 'next/types'
32

33 34 35 36 37 38 39 40 41
export type ManifestItem = {
  id: number | string
  name: string
  file: string
  publicPath: string
}

type ReactLoadableManifest = { [moduleId: string]: ManifestItem[] }

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 55 56
  // 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()

57
  constructor(pathname: string, query: ParsedUrlQuery, as: string) {
T
Tim Neutkens 已提交
58
    this.route = pathname.replace(/\/$/, '') || '/'
59 60 61 62
    this.pathname = pathname
    this.query = query
    this.asPath = as
  }
63
  push(): any {
64 65
    noRouter()
  }
66
  replace(): any {
67 68 69 70 71 72 73 74
    noRouter()
  }
  reload() {
    noRouter()
  }
  back() {
    noRouter()
  }
75
  prefetch(): any {
76 77 78 79 80 81 82
    noRouter()
  }
  beforePopState() {
    noRouter()
  }
}

83 84
function enhanceComponents(
  options: ComponentsEnhancer,
85
  App: AppType,
J
Joe Haddad 已提交
86
  Component: NextComponentType
87
): {
J
Joe Haddad 已提交
88 89
  App: AppType
  Component: NextComponentType
90 91
} {
  // For backwards compatibility
92
  if (typeof options === 'function') {
93
    return {
94 95
      App,
      Component: options(Component),
96 97 98 99 100
    }
  }

  return {
    App: options.enhanceApp ? options.enhanceApp(App) : App,
101 102 103
    Component: options.enhanceComponent
      ? options.enhanceComponent(Component)
      : Component,
104 105 106
  }
}

107 108 109
function render(
  renderElementToString: (element: React.ReactElement<any>) => string,
  element: React.ReactElement<any>,
J
Joe Haddad 已提交
110
  ampMode: any
111
): { html: string; head: React.ReactElement[] } {
112 113 114 115 116 117
  let html
  let head

  try {
    html = renderElementToString(element)
  } finally {
118
    head = Head.rewind() || defaultHead(isInAmpMode(ampMode))
119 120 121 122 123 124
  }

  return { html, head }
}

type RenderOpts = {
125
  documentMiddlewareEnabled: boolean
T
Tim Neutkens 已提交
126
  ampBindInitData: boolean
127 128
  staticMarkup: boolean
  buildId: string
129
  canonicalBase: string
130
  runtimeConfig?: { [key: string]: any }
131
  dangerousAsPath: string
132 133
  assetPrefix?: string
  err?: Error | null
134
  autoExport?: boolean
135
  nextExport?: boolean
136
  skeleton?: boolean
137
  dev?: boolean
J
Joe Haddad 已提交
138
  ampMode?: any
139
  ampPath?: string
J
Joe Haddad 已提交
140
  dataOnly?: boolean
141 142
  inAmpMode?: boolean
  hybridAmp?: boolean
143 144
  buildManifest: BuildManifest
  reactLoadableManifest: ReactLoadableManifest
145
  pageConfig: PageConfig
146
  Component: React.ComponentType
147
  Document: DocumentType
J
JJ Kasper 已提交
148
  DocumentMiddleware: (ctx: NextPageContext) => void
149
  App: AppType
J
Joe Haddad 已提交
150 151
  ErrorDebug?: React.ComponentType<{ error: Error }>
  ampValidator?: (html: string, pathname: string) => Promise<void>
152 153
}

154
function renderDocument(
155
  Document: DocumentType,
156
  {
T
Tim Neutkens 已提交
157
    dataManagerData,
158 159 160 161 162
    props,
    docProps,
    pathname,
    query,
    buildId,
163
    canonicalBase,
164 165 166
    assetPrefix,
    runtimeConfig,
    nextExport,
167
    autoExport,
168
    skeleton,
169
    dynamicImportsIds,
170
    dangerousAsPath,
171 172
    err,
    dev,
173
    ampPath,
174 175 176
    ampState,
    inAmpMode,
    hybridAmp,
177 178 179 180 181
    staticMarkup,
    devFiles,
    files,
    dynamicImports,
  }: RenderOpts & {
J
Joe Haddad 已提交
182
    dataManagerData: string
183
    props: any
184
    docProps: DocumentInitialProps
185 186
    pathname: string
    query: ParsedUrlQuery
187
    dangerousAsPath: string
188
    ampState: any
J
Joe Haddad 已提交
189
    ampPath: string
190 191
    inAmpMode: boolean
    hybridAmp: boolean
192 193 194
    dynamicImportsIds: string[]
    dynamicImports: ManifestItem[]
    files: string[]
J
Joe Haddad 已提交
195 196
    devFiles: string[]
  }
197 198 199 200
): string {
  return (
    '<!DOCTYPE html>' +
    renderToStaticMarkup(
201
      <AmpStateContext.Provider value={ampState}>
202 203 204 205 206 207 208 209 210 211
        <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`
212
            autoExport, // If this is an auto exported page
213
            skeleton, // If this is a skeleton page for experimentalPrerender
J
Joe Haddad 已提交
214 215
            dynamicIds:
              dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds,
216 217
            err: err ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML
          }}
218
          dangerousAsPath={dangerousAsPath}
219
          canonicalBase={canonicalBase}
220
          ampPath={ampPath}
221 222
          inAmpMode={inAmpMode}
          hybridAmp={hybridAmp}
223 224 225 226 227 228 229
          staticMarkup={staticMarkup}
          devFiles={devFiles}
          files={files}
          dynamicImports={dynamicImports}
          assetPrefix={assetPrefix}
          {...docProps}
        />
230
      </AmpStateContext.Provider>
231
    )
232 233 234
  )
}

235 236 237 238 239
export async function renderToHTML(
  req: IncomingMessage,
  res: ServerResponse,
  pathname: string,
  query: ParsedUrlQuery,
J
Joe Haddad 已提交
240
  renderOpts: RenderOpts
241
): Promise<string | null> {
242
  pathname = pathname === '/index' ? '/' : pathname
243 244 245
  const {
    err,
    dev = false,
246
    documentMiddlewareEnabled = false,
T
Tim Neutkens 已提交
247
    ampBindInitData = false,
248
    staticMarkup = false,
249
    ampPath = '',
250 251
    App,
    Document,
252
    pageConfig = {},
J
JJ Kasper 已提交
253
    DocumentMiddleware,
254 255 256
    Component,
    buildManifest,
    reactLoadableManifest,
257
    ErrorDebug,
258
  } = renderOpts
259

260
  await Loadable.preloadAll() // Make sure all dynamic imports are loaded
261 262 263 264 265 266 267 268 269 270 271 272 273 274

  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
275

276 277 278
  if (dev) {
    const { isValidElementType } = require('react-is')
    if (!isValidElementType(Component)) {
279
      throw new Error(
J
Joe Haddad 已提交
280
        `The default export is not a React Component in page: "${pathname}"`
281
      )
282 283
    }

284
    if (!isValidElementType(App)) {
285
      throw new Error(
J
Joe Haddad 已提交
286
        `The default export is not a React Component in page: "/_app"`
287
      )
288 289 290
    }

    if (!isValidElementType(Document)) {
291
      throw new Error(
J
Joe Haddad 已提交
292
        `The default export is not a React Component in page: "/_document"`
293
      )
294
    }
J
JJ Kasper 已提交
295

296 297 298 299
    if (isStaticPage) {
      // remove query values except ones that will be set during export
      query = {
        amp: query.amp,
J
JJ Kasper 已提交
300
      }
301
      req.url = pathname
302
      renderOpts.nextExport = true
J
JJ Kasper 已提交
303
    }
304
  }
305
  if (isSkeleton) renderOpts.nextExport = true
306
  if (isAutoExport) renderOpts.autoExport = true
307

308 309
  // @ts-ignore url will always be set
  const asPath: string = req.url
310
  const router = new ServerRouter(pathname, query, asPath)
311 312 313 314 315 316 317
  const ctx = {
    err,
    req: isStaticPage ? undefined : req,
    res: isStaticPage ? undefined : res,
    pathname,
    query,
    asPath,
318 319 320 321 322 323 324
    AppTree: (props: any) => {
      return (
        <AppContainer>
          <App {...props} Component={Component} router={router} />
        </AppContainer>
      )
    },
325
  }
326
  let props: any
327 328 329
  const isDataPrerender =
    pageConfig.experimentalPrerender === true &&
    req.headers['content-type'] === 'application/json'
330

331
  if (documentMiddlewareEnabled && typeof DocumentMiddleware === 'function') {
J
JJ Kasper 已提交
332 333 334
    await DocumentMiddleware(ctx)
  }

335 336 337 338 339 340 341 342 343 344 345 346 347 348
  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) => (
349 350 351 352 353 354 355 356 357 358 359
    <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>
360 361
  )

362
  try {
363 364 365 366
    props =
      isSkeleton && !isDataPrerender
        ? { pageProps: {} }
        : await loadGetInitialProps(App, {
367
            AppTree: ctx.AppTree,
368 369 370 371
            Component,
            router,
            ctx,
          })
372 373 374 375 376
  } catch (err) {
    if (!dev || !err) throw err
    ctx.err = err
    renderOpts.err = err
  }
377

378
  if (isDataPrerender) {
379 380 381 382 383
    res.setHeader('content-type', 'application/json')
    res.end(JSON.stringify(props.pageProps || {}))
    return null
  }

384
  // the response might be finished on the getInitialProps call
385 386 387 388 389 390 391
  if (isResSent(res)) return null

  const devFiles = buildManifest.devFiles
  const files = [
    ...new Set([
      ...getPageFiles(buildManifest, pathname),
      ...getPageFiles(buildManifest, '/_app'),
392
    ]),
393 394
  ]

T
Tim Neutkens 已提交
395 396 397 398
  const renderElementToString = staticMarkup
    ? renderToStaticMarkup
    : renderToString

J
Joe Haddad 已提交
399
  const renderPageError = (): { html: string; head: any } | void => {
400
    if (ctx.err && ErrorDebug) {
J
Joe Haddad 已提交
401 402 403
      return render(
        renderElementToString,
        <ErrorDebug error={ctx.err} />,
404
        ampState
J
Joe Haddad 已提交
405
      )
406 407 408 409
    }

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

415
  let renderPage: RenderPage
T
Tim Neutkens 已提交
416 417

  if (ampBindInitData) {
418 419
    const ssrPrepass = require('react-ssr-prepass')

T
Tim Neutkens 已提交
420
    renderPage = async (
J
Joe Haddad 已提交
421 422
      options: ComponentsEnhancer = {}
    ): Promise<{ html: string; head: any; dataOnly?: true }> => {
423 424
      const renderError = renderPageError()
      if (renderError) return renderError
T
Tim Neutkens 已提交
425 426 427 428 429 430

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

J
Joe Haddad 已提交
431
      const Application = () => (
432 433 434 435 436 437 438
        <AppContainer>
          <EnhancedApp
            Component={EnhancedComponent}
            router={router}
            {...props}
          />
        </AppContainer>
J
Joe Haddad 已提交
439
      )
440

J
Joe Haddad 已提交
441
      const element = <Application />
442 443

      try {
444
        return render(renderElementToString, element, ampState)
445 446 447 448 449 450 451 452 453 454
      } catch (err) {
        if (err && typeof err === 'object' && typeof err.then === 'function') {
          await ssrPrepass(element)
          if (renderOpts.dataOnly) {
            return {
              html: '',
              head: [],
              dataOnly: true,
            }
          } else {
455
            return render(renderElementToString, element, ampState)
456
          }
T
Tim Neutkens 已提交
457
        }
458
        throw err
T
Tim Neutkens 已提交
459 460 461 462
      }
    }
  } else {
    renderPage = (
J
Joe Haddad 已提交
463
      options: ComponentsEnhancer = {}
464 465 466
    ): { html: string; head: any } => {
      const renderError = renderPageError()
      if (renderError) return renderError
467

468 469 470 471 472 473 474
      const {
        App: EnhancedApp,
        Component: EnhancedComponent,
      } = enhanceComponents(options, App, Component)

      return render(
        renderElementToString,
475 476 477 478 479 480 481
        <AppContainer>
          <EnhancedApp
            Component={EnhancedComponent}
            router={router}
            {...props}
          />
        </AppContainer>,
482
        ampState
J
Joe Haddad 已提交
483
      )
484
    }
T
Tim Neutkens 已提交
485
  }
486 487

  const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage })
K
Kévin Dunglas 已提交
488
  // the response might be finished on the getInitialProps call
489 490
  if (isResSent(res)) return null

T
Tim Neutkens 已提交
491 492 493 494 495
  let dataManagerData = '[]'
  if (dataManager) {
    dataManagerData = JSON.stringify([...dataManager.getData()])
  }

496
  if (!docProps || typeof docProps.html !== 'string') {
J
Joe Haddad 已提交
497 498 499
    const message = `"${getDisplayName(
      Document
    )}.getInitialProps()" should resolve to an object with a "html" prop set with a valid html string`
500 501 502
    throw new Error(message)
  }

503
  if (docProps.dataOnly) {
T
Tim Neutkens 已提交
504 505 506
    return dataManagerData
  }

507 508 509 510 511 512 513 514 515 516 517 518 519 520 521
  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]
522 523 524
  const inAmpMode = isInAmpMode(ampState)
  const hybridAmp = ampState.hybrid

525
  // update renderOpts so export knows current state
526 527
  renderOpts.inAmpMode = inAmpMode
  renderOpts.hybridAmp = hybridAmp
528
  if (isSkeleton) renderOpts.skeleton = true
529

J
JJ Kasper 已提交
530
  let html = renderDocument(Document, {
531
    ...renderOpts,
532
    dangerousAsPath: router.asPath,
T
Tim Neutkens 已提交
533
    dataManagerData,
534
    ampState,
535 536 537
    props,
    docProps,
    pathname,
538
    ampPath,
539
    query,
540 541
    inAmpMode,
    hybridAmp,
542 543 544
    dynamicImportsIds,
    dynamicImports,
    files,
545
    devFiles,
546
  })
J
JJ Kasper 已提交
547

548
  if (inAmpMode && html) {
549
    // use replace to allow rendering directly to body in AMP mode
550 551 552 553
    html = html.replace(
      '__NEXT_AMP_RENDER_TARGET__',
      `<!-- __NEXT_DATA__ -->${docProps.html}`
    )
554
    html = await optimizeAmp(html)
555

556
    if (renderOpts.ampValidator) {
557 558
      await renderOpts.ampValidator(html, pathname)
    }
J
JJ Kasper 已提交
559
  }
J
JJ Kasper 已提交
560

561
  if (inAmpMode || hybridAmp) {
J
JJ Kasper 已提交
562 563 564
    // fix &amp being escaped for amphtml rel link
    html = html.replace(/&amp;amp=1/g, '&amp=1')
  }
J
JJ Kasper 已提交
565
  return html
566 567
}

568
function errorToJSON(err: Error): Error {
569 570 571 572
  const { name, message, stack } = err
  return { name, message, stack }
}

573 574
function serializeError(
  dev: boolean | undefined,
J
Joe Haddad 已提交
575
  err: Error
576
): Error & { statusCode?: number } {
577 578 579 580
  if (dev) {
    return errorToJSON(err)
  }

581 582 583 584 585
  return {
    name: 'Internal Server Error.',
    message: '500 - Internal Server Error.',
    statusCode: 500,
  }
586
}