import { join } from 'path' import React from 'react' import { renderToString, renderToStaticMarkup } from 'react-dom/server' import send from 'send' import generateETag from 'etag' import fresh from 'fresh' import requirePage, {normalizePagePath} from './require' import { Router } from '../lib/router' import { loadGetInitialProps, isResSent } from '../lib/utils' import Head, { defaultHead } from '../lib/head' import ErrorDebug from '../lib/error-debug' import Loadable from 'react-loadable' import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH } from '../lib/constants' // Based on https://github.com/jamiebuilds/react-loadable/pull/132 function getDynamicImportBundles (manifest, moduleIds) { return moduleIds.reduce((bundles, moduleId) => { if (typeof manifest[moduleId] === 'undefined') { return bundles } return bundles.concat(manifest[moduleId]) }, []) } const logger = console export async function render (req, res, pathname, query, opts) { const html = await renderToHTML(req, res, pathname, query, opts) sendHTML(req, res, html, req.method, opts) } export function renderToHTML (req, res, pathname, query, opts) { return doRender(req, res, pathname, query, opts) } export async function renderError (err, req, res, pathname, query, opts) { const html = await renderErrorToHTML(err, req, res, query, opts) sendHTML(req, res, html, req.method, opts) } export function renderErrorToHTML (err, req, res, pathname, query, opts = {}) { return doRender(req, res, pathname, query, { ...opts, err, page: '/_error' }) } async function doRender (req, res, pathname, query, { err, page, buildId, hotReloader, assetPrefix, runtimeConfig, distDir, dir, dev = false, staticMarkup = false, nextExport } = {}) { page = page || pathname if (hotReloader) { // In dev mode we use on demand entries to compile the page before rendering await ensurePage(page, { dir, hotReloader }) } const documentPath = join(distDir, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH, buildId, 'pages', '_document') const appPath = join(distDir, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH, buildId, 'pages', '_app') let [buildManifest, reactLoadableManifest, Component, Document, App] = await Promise.all([ require(join(distDir, BUILD_MANIFEST)), require(join(distDir, REACT_LOADABLE_MANIFEST)), requirePage(page, {distDir}), require(documentPath), require(appPath) ]) await Loadable.preloadAll() // Make sure all dynamic imports are loaded Component = Component.default || Component if (typeof Component !== 'function') { throw new Error(`The default export is not a React Component in page: "${pathname}"`) } App = App.default || App Document = Document.default || Document const asPath = req.url const ctx = { err, req, res, pathname, query, asPath } const router = new Router(pathname, query, asPath) const props = await loadGetInitialProps(App, {Component, router, ctx}) const files = [ ...new Set([ ...buildManifest.pages[normalizePagePath(page)], ...buildManifest.pages[normalizePagePath('/_app')], ...buildManifest.pages[normalizePagePath('/_error')] ]) ] // the response might be finshed on the getinitialprops call if (isResSent(res)) return let reactLoadableModules = [] const renderPage = (options = Page => Page) => { let EnhancedApp = App let EnhancedComponent = Component // For backwards compatibility if (typeof options === 'function') { EnhancedComponent = options(Component) } else if (typeof options === 'object') { if (options.enhanceApp) { EnhancedApp = options.enhanceApp(App) } if (options.enhanceComponent) { EnhancedComponent = options.enhanceComponent(Component) } } const app = reactLoadableModules.push(moduleName)}> const render = staticMarkup ? renderToStaticMarkup : renderToString let html let head let errorHtml = '' try { if (err && dev) { errorHtml = render() } else if (err) { html = render(app) } else { html = render(app) } } finally { head = Head.rewind() || defaultHead() } return { html, head, errorHtml, buildManifest } } const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage }) const dynamicImports = getDynamicImportBundles(reactLoadableManifest, reactLoadableModules) if (isResSent(res)) return if (!Document.prototype || !Document.prototype.isReactComponent) throw new Error('_document.js is not exporting a React component') const doc = return '' + renderToStaticMarkup(doc) } export async function renderScriptError (req, res, page, error) { // Asks CDNs and others to not to cache the errored page res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate') if (error.code === 'ENOENT' || error.message === 'INVALID_BUILD_ID') { res.statusCode = 404 res.end('404 - Not Found') return } logger.error(error.stack) res.statusCode = 500 res.end('500 - Internal Error') } export function sendHTML (req, res, html, method, { dev, generateEtags }) { if (isResSent(res)) return const etag = generateEtags && generateETag(html) if (fresh(req.headers, { etag })) { res.statusCode = 304 res.end() return } if (dev) { // In dev, we should not cache pages for any reason. // That's why we do this. res.setHeader('Cache-Control', 'no-store, must-revalidate') } if (etag) { res.setHeader('ETag', etag) } if (!res.getHeader('Content-Type')) { res.setHeader('Content-Type', 'text/html; charset=utf-8') } res.setHeader('Content-Length', Buffer.byteLength(html)) res.end(method === 'HEAD' ? null : html) } function errorToJSON (err) { const { name, message, stack } = err const json = { name, message, stack } if (err.module) { // rawRequest contains the filename of the module which has the error. const { rawRequest } = err.module json.module = { rawRequest } } return json } function serializeError (dev, err) { if (dev) { return errorToJSON(err) } return { message: '500 - Internal Server Error.' } } export function serveStatic (req, res, path) { return new Promise((resolve, reject) => { send(req, path) .on('directory', () => { // We don't allow directories to be read. const err = new Error('No directory access') err.code = 'ENOENT' reject(err) }) .on('error', reject) .pipe(res) .on('finish', resolve) }) } async function ensurePage (page, { dir, hotReloader }) { if (page === '/_error') return await hotReloader.ensurePage(page) }