render.tsx 15.3 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'
J
JJ Kasper 已提交
32 33
import { isDynamicRoute } from '../lib/router/utils/is-dynamic'
import { SPR_GET_INITIAL_PROPS_CONFLICT } from '../../lib/constants'
34

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

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

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

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

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

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

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

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

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

  return { html, head }
}

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

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

247 248 249 250 251
export async function renderToHTML(
  req: IncomingMessage,
  res: ServerResponse,
  pathname: string,
  query: ParsedUrlQuery,
J
Joe Haddad 已提交
252
  renderOpts: RenderOpts
253
): Promise<string | null> {
254
  pathname = pathname === '/index' ? '/' : pathname
255 256 257
  const {
    err,
    dev = false,
258
    documentMiddlewareEnabled = false,
T
Tim Neutkens 已提交
259
    ampBindInitData = false,
260
    staticMarkup = false,
261
    ampPath = '',
262 263
    App,
    Document,
264
    pageConfig = {},
J
JJ Kasper 已提交
265
    DocumentMiddleware,
266 267 268
    Component,
    buildManifest,
    reactLoadableManifest,
269
    ErrorDebug,
J
JJ Kasper 已提交
270
    unstable_getStaticProps,
271
  } = renderOpts
272

J
JJ Kasper 已提交
273
  const isSpr = !!unstable_getStaticProps
274 275 276
  const defaultAppGetInitialProps =
    App.getInitialProps === (App as any).origGetInitialProps

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

J
JJ Kasper 已提交
279 280 281 282 283 284
  const isAutoExport =
    !hasPageGetInitialProps && defaultAppGetInitialProps && !isSpr

  if (hasPageGetInitialProps && isSpr) {
    throw new Error(SPR_GET_INITIAL_PROPS_CONFLICT + ` ${pathname}`)
  }
285

286 287 288
  if (dev) {
    const { isValidElementType } = require('react-is')
    if (!isValidElementType(Component)) {
289
      throw new Error(
J
Joe Haddad 已提交
290
        `The default export is not a React Component in page: "${pathname}"`
291
      )
292 293
    }

294
    if (!isValidElementType(App)) {
295
      throw new Error(
J
Joe Haddad 已提交
296
        `The default export is not a React Component in page: "/_app"`
297
      )
298 299 300
    }

    if (!isValidElementType(Document)) {
301
      throw new Error(
J
Joe Haddad 已提交
302
        `The default export is not a React Component in page: "/_document"`
303
      )
304
    }
J
JJ Kasper 已提交
305

J
JJ Kasper 已提交
306
    if (isAutoExport) {
307 308 309
      // remove query values except ones that will be set during export
      query = {
        amp: query.amp,
J
JJ Kasper 已提交
310
      }
311
      req.url = pathname
312
      renderOpts.nextExport = true
J
JJ Kasper 已提交
313
    }
314
  }
315
  if (isAutoExport) renderOpts.autoExport = true
316

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

319 320
  // @ts-ignore url will always be set
  const asPath: string = req.url
321
  const router = new ServerRouter(pathname, query, asPath)
322 323
  const ctx = {
    err,
J
JJ Kasper 已提交
324 325
    req: isAutoExport ? undefined : req,
    res: isAutoExport ? undefined : res,
326 327 328
    pathname,
    query,
    asPath,
329 330 331 332 333 334 335
    AppTree: (props: any) => {
      return (
        <AppContainer>
          <App {...props} Component={Component} router={router} />
        </AppContainer>
      )
    },
336
  }
337
  let props: any
338

339
  if (documentMiddlewareEnabled && typeof DocumentMiddleware === 'function') {
J
JJ Kasper 已提交
340 341 342
    await DocumentMiddleware(ctx)
  }

343 344 345 346 347 348 349 350 351 352 353 354 355 356
  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) => (
357 358 359 360 361 362 363 364 365 366 367
    <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>
368 369
  )

370
  try {
J
JJ Kasper 已提交
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386
    props = await loadGetInitialProps(App, {
      AppTree: ctx.AppTree,
      Component,
      router,
      ctx,
    })

    if (isSpr) {
      const data = await unstable_getStaticProps!({
        params: isDynamicRoute(pathname) ? query : undefined,
      })
      props.pageProps = data.props
      // pass up revalidate and props for export
      ;(renderOpts as any).revalidate = data.revalidate
      ;(renderOpts as any).sprData = props
    }
387 388 389 390 391
  } catch (err) {
    if (!dev || !err) throw err
    ctx.err = err
    renderOpts.err = err
  }
392

393
  // the response might be finished on the getInitialProps call
J
JJ Kasper 已提交
394
  if (isResSent(res) && !isSpr) return null
395 396 397 398 399 400

  const devFiles = buildManifest.devFiles
  const files = [
    ...new Set([
      ...getPageFiles(buildManifest, pathname),
      ...getPageFiles(buildManifest, '/_app'),
401
    ]),
402 403
  ]

T
Tim Neutkens 已提交
404 405 406 407
  const renderElementToString = staticMarkup
    ? renderToStaticMarkup
    : renderToString

J
Joe Haddad 已提交
408
  const renderPageError = (): { html: string; head: any } | void => {
409
    if (ctx.err && ErrorDebug) {
J
Joe Haddad 已提交
410 411 412
      return render(
        renderElementToString,
        <ErrorDebug error={ctx.err} />,
413
        ampState
J
Joe Haddad 已提交
414
      )
415 416 417 418
    }

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

424
  let renderPage: RenderPage
T
Tim Neutkens 已提交
425 426

  if (ampBindInitData) {
427 428
    const ssrPrepass = require('react-ssr-prepass')

T
Tim Neutkens 已提交
429
    renderPage = async (
J
Joe Haddad 已提交
430 431
      options: ComponentsEnhancer = {}
    ): Promise<{ html: string; head: any; dataOnly?: true }> => {
432 433
      const renderError = renderPageError()
      if (renderError) return renderError
T
Tim Neutkens 已提交
434 435 436 437 438 439

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

J
Joe Haddad 已提交
440
      const Application = () => (
441 442 443 444 445 446 447
        <AppContainer>
          <EnhancedApp
            Component={EnhancedComponent}
            router={router}
            {...props}
          />
        </AppContainer>
J
Joe Haddad 已提交
448
      )
449

J
Joe Haddad 已提交
450
      const element = <Application />
451 452

      try {
453
        return render(renderElementToString, element, ampState)
454 455 456 457 458 459 460 461 462 463
      } catch (err) {
        if (err && typeof err === 'object' && typeof err.then === 'function') {
          await ssrPrepass(element)
          if (renderOpts.dataOnly) {
            return {
              html: '',
              head: [],
              dataOnly: true,
            }
          } else {
464
            return render(renderElementToString, element, ampState)
465
          }
T
Tim Neutkens 已提交
466
        }
467
        throw err
T
Tim Neutkens 已提交
468 469 470 471
      }
    }
  } else {
    renderPage = (
J
Joe Haddad 已提交
472
      options: ComponentsEnhancer = {}
473 474 475
    ): { html: string; head: any } => {
      const renderError = renderPageError()
      if (renderError) return renderError
476

477 478 479 480 481 482 483
      const {
        App: EnhancedApp,
        Component: EnhancedComponent,
      } = enhanceComponents(options, App, Component)

      return render(
        renderElementToString,
484 485 486 487 488 489 490
        <AppContainer>
          <EnhancedApp
            Component={EnhancedComponent}
            router={router}
            {...props}
          />
        </AppContainer>,
491
        ampState
J
Joe Haddad 已提交
492
      )
493
    }
T
Tim Neutkens 已提交
494
  }
495 496

  const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage })
K
Kévin Dunglas 已提交
497
  // the response might be finished on the getInitialProps call
J
JJ Kasper 已提交
498
  if (isResSent(res) && !isSpr) return null
499

T
Tim Neutkens 已提交
500 501 502 503 504
  let dataManagerData = '[]'
  if (dataManager) {
    dataManagerData = JSON.stringify([...dataManager.getData()])
  }

505
  if (!docProps || typeof docProps.html !== 'string') {
J
Joe Haddad 已提交
506 507 508
    const message = `"${getDisplayName(
      Document
    )}.getInitialProps()" should resolve to an object with a "html" prop set with a valid html string`
509 510 511
    throw new Error(message)
  }

512
  if (docProps.dataOnly) {
T
Tim Neutkens 已提交
513 514 515
    return dataManagerData
  }

516 517 518 519 520 521 522 523 524 525 526 527 528 529 530
  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]
531 532 533
  const inAmpMode = isInAmpMode(ampState)
  const hybridAmp = ampState.hybrid

534
  // update renderOpts so export knows current state
535 536
  renderOpts.inAmpMode = inAmpMode
  renderOpts.hybridAmp = hybridAmp
537

J
JJ Kasper 已提交
538
  let html = renderDocument(Document, {
539
    ...renderOpts,
540
    dangerousAsPath: router.asPath,
T
Tim Neutkens 已提交
541
    dataManagerData,
542
    ampState,
543 544 545
    props,
    docProps,
    pathname,
546
    ampPath,
547
    query,
548 549
    inAmpMode,
    hybridAmp,
550 551 552
    dynamicImportsIds,
    dynamicImports,
    files,
553
    devFiles,
554
  })
J
JJ Kasper 已提交
555

556
  if (inAmpMode && html) {
557
    // use replace to allow rendering directly to body in AMP mode
558 559 560 561
    html = html.replace(
      '__NEXT_AMP_RENDER_TARGET__',
      `<!-- __NEXT_DATA__ -->${docProps.html}`
    )
562
    html = await optimizeAmp(html)
563

564
    if (renderOpts.ampValidator) {
565 566
      await renderOpts.ampValidator(html, pathname)
    }
J
JJ Kasper 已提交
567
  }
J
JJ Kasper 已提交
568

569
  if (inAmpMode || hybridAmp) {
J
JJ Kasper 已提交
570 571 572
    // fix &amp being escaped for amphtml rel link
    html = html.replace(/&amp;amp=1/g, '&amp=1')
  }
J
JJ Kasper 已提交
573

J
JJ Kasper 已提交
574
  return html
575 576
}

577
function errorToJSON(err: Error): Error {
578 579 580 581
  const { name, message, stack } = err
  return { name, message, stack }
}

582 583
function serializeError(
  dev: boolean | undefined,
J
Joe Haddad 已提交
584
  err: Error
585
): Error & { statusCode?: number } {
586 587 588 589
  if (dev) {
    return errorToJSON(err)
  }

590 591 592 593 594
  return {
    name: 'Internal Server Error.',
    message: '500 - Internal Server Error.',
    statusCode: 500,
  }
595
}