diff --git a/packages/next-server/lib/loadable-capture.js b/packages/next-server/lib/loadable-capture.tsx similarity index 93% rename from packages/next-server/lib/loadable-capture.js rename to packages/next-server/lib/loadable-capture.tsx index aa7eab05..1f06e69d 100644 --- a/packages/next-server/lib/loadable-capture.js +++ b/packages/next-server/lib/loadable-capture.tsx @@ -23,7 +23,11 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE import React from 'react' import PropTypes from 'prop-types' -export default class Capture extends React.Component { +type Props = { + report: (moduleName: string) => void +} + +export default class Capture extends React.Component { static propTypes = { report: PropTypes.func.isRequired }; diff --git a/packages/next-server/lib/loadable.js b/packages/next-server/lib/loadable.js index dcf87fe2..635e04de 100644 --- a/packages/next-server/lib/loadable.js +++ b/packages/next-server/lib/loadable.js @@ -315,4 +315,4 @@ Loadable.preloadReady = (webpackIds) => { }) } -module.exports = Loadable +export default Loadable diff --git a/packages/next-server/package.json b/packages/next-server/package.json index 150c56c2..79f8282a 100644 --- a/packages/next-server/package.json +++ b/packages/next-server/package.json @@ -42,6 +42,8 @@ "devDependencies": { "@taskr/clear": "1.1.0", "@taskr/watch": "1.1.0", + "@types/react": "16.7.13", + "@types/react-dom": "16.0.11", "@types/send": "0.14.4", "taskr": "1.1.0", "typescript": "3.1.6" diff --git a/packages/next-server/server/get-dynamic-import-bundles.ts b/packages/next-server/server/get-dynamic-import-bundles.ts index 7af80129..30c6aaf2 100644 --- a/packages/next-server/server/get-dynamic-import-bundles.ts +++ b/packages/next-server/server/get-dynamic-import-bundles.ts @@ -1,4 +1,4 @@ -type ManifestItem = { +export type ManifestItem = { id: number|string, name: string, file: string, diff --git a/packages/next-server/server/next-server.ts b/packages/next-server/server/next-server.ts index 91f13e09..54887e78 100644 --- a/packages/next-server/server/next-server.ts +++ b/packages/next-server/server/next-server.ts @@ -4,10 +4,7 @@ import { resolve, join, sep } from 'path' import { parse as parseUrl, UrlWithParsedQuery } from 'url' import { parse as parseQs, ParsedUrlQuery } from 'querystring' import fs from 'fs' -import { - renderToHTML, - renderErrorToHTML -} from './render' +import {renderToHTML} from './render' import {sendHTML} from './send-html' import {serveStatic} from './serve-static' import Router, {route, Route} from './router' @@ -246,11 +243,14 @@ export default class Server { public async renderError (err: Error|null, req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}): Promise { res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate') const html = await this.renderErrorToHTML(err, req, res, pathname, query) + if(html === null) { + return + } return this.sendHTML(req, res, html) } public async renderErrorToHTML (err: Error|null, req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery = {}) { - return renderErrorToHTML(err, req, res, pathname, query, this.renderOpts) + return renderToHTML(req, res, '/_error', query, {...this.renderOpts, err}) } public async render404 (req: IncomingMessage, res: ServerResponse, parsedUrl?: UrlWithParsedQuery): Promise { diff --git a/packages/next-server/server/render.js b/packages/next-server/server/render.js deleted file mode 100644 index 59fd02ef..00000000 --- a/packages/next-server/server/render.js +++ /dev/null @@ -1,162 +0,0 @@ -import { join } from 'path' -import React from 'react' -import { renderToString, renderToStaticMarkup } from 'react-dom/server' -import {requirePage} from './require' -import Router from '../lib/router/router' -import { loadGetInitialProps, isResSent } from '../lib/utils' -import Head, { defaultHead } from '../lib/head' -import Loadable from '../lib/loadable' -import LoadableCapture from '../lib/loadable-capture' -import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH } from 'next-server/constants' -import {getDynamicImportBundles} from './get-dynamic-import-bundles' -import {getPageFiles} from './get-page-files' - -export function renderToHTML (req, res, pathname, query, opts) { - return doRender(req, res, pathname, query, opts) -} - -// _pathname is for backwards compatibility -export function renderErrorToHTML (err, req, res, _pathname, query, opts = {}) { - return doRender(req, res, '/_error', query, { ...opts, err }) -} - -async function doRender (req, res, pathname, query, { - err, - buildId, - assetPrefix, - runtimeConfig, - distDir, - dev = false, - staticMarkup = false, - nextExport -} = {}) { - 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(pathname, 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 devFiles = buildManifest.devFiles - const files = [ - ...new Set([ - ...getPageFiles(buildManifest, pathname), - ...getPageFiles(buildManifest, '/_app'), - ...getPageFiles(buildManifest, '/_error') - ]) - ] - - // the response might be finshed on the getinitialprops call - if (isResSent(res)) return null - - 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 render = staticMarkup ? renderToStaticMarkup : renderToString - - let html - let head - - try { - if (err && dev) { - const ErrorDebug = require(join(distDir, SERVER_DIRECTORY, 'error-debug')).default - html = render() - } else { - html = render( - reactLoadableModules.push(moduleName)}> - - - ) - } - } finally { - head = Head.rewind() || defaultHead() - } - - return { html, head, buildManifest } - } - - const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage }) - const dynamicImports = [...getDynamicImportBundles(reactLoadableManifest, reactLoadableModules)] - const dynamicImportsIds = dynamicImports.map((bundle) => bundle.id) - - if (isResSent(res)) return null - - if (!Document.prototype || !Document.prototype.isReactComponent) throw new Error('_document.js is not exporting a React component') - const doc = - - return '' + renderToStaticMarkup(doc) -} - -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.' } -} diff --git a/packages/next-server/server/render.tsx b/packages/next-server/server/render.tsx new file mode 100644 index 00000000..e16907a6 --- /dev/null +++ b/packages/next-server/server/render.tsx @@ -0,0 +1,208 @@ +import {IncomingMessage, ServerResponse} from 'http' +import { ParsedUrlQuery } from 'querystring' +import { join } from 'path' +import React from 'react' +import { renderToString, renderToStaticMarkup } from 'react-dom/server' +import {requirePage} from './require' +import Router from '../lib/router/router' +import { loadGetInitialProps, isResSent } from '../lib/utils' +import Head, { defaultHead } from '../lib/head' +import Loadable from '../lib/loadable' +import LoadableCapture from '../lib/loadable-capture' +import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH } from 'next-server/constants' +import {getDynamicImportBundles, ManifestItem} from './get-dynamic-import-bundles' +import {getPageFiles} from './get-page-files' + +type Enhancer = (Component: React.ComponentType) => React.ComponentType +type ComponentsEnhancer = {enhanceApp?: Enhancer, enhanceComponent?: Enhancer}|Enhancer + +function enhanceComponents(options: ComponentsEnhancer, App: React.ComponentType, Component: React.ComponentType): { + App: React.ComponentType, + Component: React.ComponentType +} { + // For backwards compatibility + if(typeof options === 'function') { + return { + App: App, + Component: options(Component) + } + } + + return { + App: options.enhanceApp ? options.enhanceApp(App) : App, + Component: options.enhanceComponent ? options.enhanceComponent(Component) : Component + } +} + +function interoptDefault(mod: any) { + return mod.default || mod +} + +function render(renderElementToString: (element: React.ReactElement) => string, element: React.ReactElement): {html: string, head: any} { + let html + let head + + try { + html = renderElementToString(element) + } finally { + head = Head.rewind() || defaultHead() + } + + return { html, head } +} + +type RenderOpts = { + staticMarkup: boolean, + distDir: string, + buildId: string, + runtimeConfig?: {[key: string]: any}, + assetPrefix?: string, + err?: Error|null, + nextExport?: boolean, + dev?: boolean +} + +function renderDocument(Document: React.ComponentType, { + props, + docProps, + pathname, + query, + buildId, + assetPrefix, + runtimeConfig, + nextExport, + dynamicImportsIds, + err, + dev, + staticMarkup, + devFiles, + files, + dynamicImports, +}: RenderOpts & { + props: any, + docProps: any, + pathname: string, + query: ParsedUrlQuery, + dynamicImportsIds: string[], + dynamicImports: ManifestItem[], + files: string[] + devFiles: string[], +}): string { + return '' + renderToStaticMarkup( + + ) +} + +export async function renderToHTML (req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery, renderOpts: RenderOpts): Promise { + const { + err, + buildId, + distDir, + dev = false, + staticMarkup = false + } = renderOpts + 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)), + interoptDefault(requirePage(pathname, distDir)), + interoptDefault(require(documentPath)), + interoptDefault(require(appPath)) + ]) + + await Loadable.preloadAll() // Make sure all dynamic imports are loaded + + if (typeof Component !== 'function') { + throw new Error(`The default export is not a React Component in page: "${pathname}"`) + } + if (!Document.prototype || !Document.prototype.isReactComponent) throw new Error('_document.js is not exporting a React component') + + 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}) + + // the response might be finshed on the getInitialProps call + if (isResSent(res)) return null + + const devFiles = buildManifest.devFiles + const files = [ + ...new Set([ + ...getPageFiles(buildManifest, pathname), + ...getPageFiles(buildManifest, '/_app'), + ...getPageFiles(buildManifest, '/_error') + ]) + ] + + const reactLoadableModules: string[] = [] + const renderPage = (options: ComponentsEnhancer = {}): {html: string, head: any} => { + const {App: EnhancedApp, Component: EnhancedComponent} = enhanceComponents(options, App, Component) + const renderElementToString = staticMarkup ? renderToStaticMarkup : renderToString + + if(err && dev) { + const ErrorDebug = require(join(distDir, SERVER_DIRECTORY, 'error-debug')).default + return render(renderElementToString, ) + } + + return render(renderElementToString, + reactLoadableModules.push(moduleName)}> + + + ) + } + + const docProps = await loadGetInitialProps(Document, { ...ctx, renderPage }) + // the response might be finshed on the getInitialProps call + if (isResSent(res)) return null + + const dynamicImports = [...getDynamicImportBundles(reactLoadableManifest, reactLoadableModules)] + const dynamicImportsIds: any = dynamicImports.map((bundle) => bundle.id) + + return renderDocument(Document, { + ...renderOpts, + props, + docProps, + pathname, + query, + dynamicImportsIds, + dynamicImports, + files, + devFiles + }) +} + +function errorToJSON (err: Error): Error { + const { name, message, stack } = err + return { name, message, stack } +} + +function serializeError (dev: boolean|undefined, err: Error): Error & {statusCode?: number} { + if (dev) { + return errorToJSON(err) + } + + return { name: 'Internal Server Error.', message: '500 - Internal Server Error.', statusCode: 500 } +} diff --git a/packages/next-server/server/require.ts b/packages/next-server/server/require.ts index ab60f74e..3ea44abc 100644 --- a/packages/next-server/server/require.ts +++ b/packages/next-server/server/require.ts @@ -45,7 +45,7 @@ export function getPagePath (page: string, distDir: string): string { return join(serverBuildPath, pagesManifest[page]) } -export async function requirePage (page: string, distDir: string): Promise { +export function requirePage (page: string, distDir: string): any { const pagePath = getPagePath(page, distDir) return require(pagePath) } diff --git a/packages/next-server/taskfile.js b/packages/next-server/taskfile.js index efc3185f..12aad7e9 100644 --- a/packages/next-server/taskfile.js +++ b/packages/next-server/taskfile.js @@ -1,12 +1,12 @@ const notifier = require('node-notifier') export async function lib (task, opts) { - await task.source(opts.src || 'lib/**/*.+(js|ts)').typescript({module: 'commonjs'}).target('dist/lib') + await task.source(opts.src || 'lib/**/*.+(js|ts|tsx)').typescript({module: 'commonjs'}).target('dist/lib') notify('Compiled lib files') } export async function server (task, opts) { - await task.source(opts.src || 'server/**/*.+(js|ts)').typescript({module: 'commonjs'}).target('dist/server') + await task.source(opts.src || 'server/**/*.+(js|ts|tsx)').typescript({module: 'commonjs'}).target('dist/server') notify('Compiled server files') } @@ -17,8 +17,8 @@ export async function build (task) { export default async function (task) { await task.clear('dist') await task.start('build') - await task.watch('server/**/*.+(js|ts)', 'server') - await task.watch('lib/**/*.+(js|ts)', 'lib') + await task.watch('server/**/*.+(js|ts|tsx)', 'server') + await task.watch('lib/**/*.+(js|ts|tsx)', 'lib') } export async function release (task) { diff --git a/packages/next/pages/_document.js b/packages/next/pages/_document.js index 1efe80b6..53fa9717 100644 --- a/packages/next/pages/_document.js +++ b/packages/next/pages/_document.js @@ -14,9 +14,9 @@ export default class Document extends Component { } static getInitialProps ({ renderPage }) { - const { html, head, buildManifest } = renderPage() + const { html, head } = renderPage() const styles = flush() - return { html, head, styles, buildManifest } + return { html, head, styles } } getChildContext () { diff --git a/packages/next/server/error-debug.js b/packages/next/server/error-debug.js index 0a7a5a1b..44f6f7c1 100644 --- a/packages/next/server/error-debug.js +++ b/packages/next/server/error-debug.js @@ -5,13 +5,12 @@ import Head from 'next-server/head' // This component is rendered through dev-error-overlay on the client side. // On the server side it's rendered directly export default function ErrorDebug ({error, info}) { - const { name, message, module } = error + const { name, message } = error return (
- {module ?

Error in {module.rawRequest}

: null} { name === 'ModuleBuildError' && message ?
diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js
index 5d8bf765..9b7af2d5 100644
--- a/packages/next/taskfile.js
+++ b/packages/next/taskfile.js
@@ -10,33 +10,33 @@ export async function bin (task, opts) {
 }
 
 export async function lib (task, opts) {
-  await task.source(opts.src || 'lib/**/*.+(js|ts)').typescript({module: 'commonjs'}).target('dist/lib')
+  await task.source(opts.src || 'lib/**/*.+(js|ts|tsx)').typescript({module: 'commonjs'}).target('dist/lib')
   notify('Compiled lib files')
 }
 
 export async function server (task, opts) {
-  await task.source(opts.src || 'server/**/*.+(js|ts)').typescript({module: 'commonjs'}).target('dist/server')
+  await task.source(opts.src || 'server/**/*.+(js|ts|tsx)').typescript({module: 'commonjs'}).target('dist/server')
   notify('Compiled server files')
 }
 
 export async function nextbuild (task, opts) {
-  await task.source(opts.src || 'build/**/*.+(js|ts)').typescript({module: 'commonjs'}).target('dist/build')
+  await task.source(opts.src || 'build/**/*.+(js|ts|tsx)').typescript({module: 'commonjs'}).target('dist/build')
   notify('Compiled build files')
 }
 
 export async function client (task, opts) {
-  await task.source(opts.src || 'client/**/*.+(js|ts)').typescript().target('dist/client')
+  await task.source(opts.src || 'client/**/*.+(js|ts|tsx)').typescript().target('dist/client')
   notify('Compiled client files')
 }
 
 // export is a reserved keyword for functions
 export async function nextbuildstatic (task, opts) {
-  await task.source(opts.src || 'export/**/*.+(js|ts)').typescript({module: 'commonjs'}).target('dist/export')
+  await task.source(opts.src || 'export/**/*.+(js|ts|tsx)').typescript({module: 'commonjs'}).target('dist/export')
   notify('Compiled export files')
 }
 
 export async function pages (task, opts) {
-  await task.source(opts.src || 'pages/**/*.+(js|ts)').typescript().target('dist/pages')
+  await task.source(opts.src || 'pages/**/*.+(js|ts|tsx)').typescript().target('dist/pages')
 }
 
 export async function build (task) {
@@ -47,12 +47,12 @@ export default async function (task) {
   await task.clear('dist')
   await task.start('build')
   await task.watch('bin/*', 'bin')
-  await task.watch('pages/**/*.+(js|ts)', 'pages')
-  await task.watch('server/**/*.+(js|ts)', 'server')
-  await task.watch('build/**/*.+(js|ts)', 'nextbuild')
-  await task.watch('export/**/*.+(js|ts)', 'nextexport')
-  await task.watch('client/**/*.+(js|ts)', 'client')
-  await task.watch('lib/**/*.+(js|ts)', 'lib')
+  await task.watch('pages/**/*.+(js|ts|tsx)', 'pages')
+  await task.watch('server/**/*.+(js|ts|tsx)', 'server')
+  await task.watch('build/**/*.+(js|ts|tsx)', 'nextbuild')
+  await task.watch('export/**/*.+(js|ts|tsx)', 'nextexport')
+  await task.watch('client/**/*.+(js|ts|tsx)', 'client')
+  await task.watch('lib/**/*.+(js|ts|tsx)', 'lib')
 }
 
 export async function release (task) {