From a0945c7800470de65f9847cb3bf5cff684ce681a Mon Sep 17 00:00:00 2001 From: Arunoda Susiripala Date: Mon, 3 Apr 2017 23:40:24 +0530 Subject: [PATCH 01/40] Introduce script tag based page loading system. --- client/index.js | 6 ++- lib/page-loader.js | 64 +++++++++++++++++++++++ server/build/plugins/json-pages-plugin.js | 2 +- server/build/plugins/pages-plugin.js | 34 ++++++++++++ server/build/webpack.js | 2 + server/index.js | 24 ++++++++- 6 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 lib/page-loader.js create mode 100644 server/build/plugins/pages-plugin.js diff --git a/client/index.js b/client/index.js index 4d5c847a81..73e36f93a7 100644 --- a/client/index.js +++ b/client/index.js @@ -7,6 +7,7 @@ import App from '../lib/app' import evalScript from '../lib/eval-script' import { loadGetInitialProps, getURL } from '../lib/utils' import ErrorDebugComponent from '../lib/error-debug' +import PageLoader from '../lib/page-loader' // Polyfill Promise globally // This is needed because Webpack2's dynamic loading(common chunks) code @@ -24,11 +25,14 @@ const { props, err, pathname, - query + query, + buildId }, location } = window +window.NEXT_PAGE_LOADER = new PageLoader(buildId) + const Component = evalScript(component).default const ErrorComponent = evalScript(errorComponent).default let lastAppProps diff --git a/lib/page-loader.js b/lib/page-loader.js new file mode 100644 index 0000000000..6964d30ba8 --- /dev/null +++ b/lib/page-loader.js @@ -0,0 +1,64 @@ +/* global window, document */ +import mitt from 'mitt' + +export default class PageLoader { + constructor (buildId) { + this.buildId = buildId + this.pageCache = {} + this.pageLoadedHandlers = {} + this.registerEvents = mitt() + this.loadingRoutes = {} + } + + loadPage (route) { + if (route[0] !== '/') { + throw new Error('Route name should start with a "/"') + } + + route = route.replace(/index$/, '') + + if (this.pageCache[route]) { + return Promise.resolve(this.pageCache[route]) + } + + return new Promise((resolve, reject) => { + const fire = ({ error, page }) => { + this.registerEvents.off(route, fire) + + if (error) { + reject(error) + } else { + resolve(page) + } + } + + this.registerEvents.on(route, fire) + + // Load the script if not asked to load yet. + if (!this.loadingRoutes[route]) { + this.loadScript(route) + this.loadingRoutes[route] = true + } + }) + } + + loadScript (route) { + const script = document.createElement('script') + const url = `/_next/${encodeURIComponent(this.buildId)}/page${route}` + script.src = url + script.type = 'text/javascript' + script.onerror = () => { + const error = new Error(`Error when loading route: ${route}`) + this.registerEvents.emit(route, { error }) + } + + document.body.appendChild(script) + } + + // This method if called by the route code. + registerPage (route, error, page) { + // add the page to the cache + this.pageCache[route] = page + this.registerEvents.emit(route, { error, page }) + } +} diff --git a/server/build/plugins/json-pages-plugin.js b/server/build/plugins/json-pages-plugin.js index 06e34851b5..c0c53434e9 100644 --- a/server/build/plugins/json-pages-plugin.js +++ b/server/build/plugins/json-pages-plugin.js @@ -7,7 +7,7 @@ export default class JsonPagesPlugin { pages.forEach((pageName) => { const page = compilation.assets[pageName] - delete compilation.assets[pageName] + // delete compilation.assets[pageName] const content = page.source() const newContent = JSON.stringify({ component: content }) diff --git a/server/build/plugins/pages-plugin.js b/server/build/plugins/pages-plugin.js new file mode 100644 index 0000000000..c5dab55732 --- /dev/null +++ b/server/build/plugins/pages-plugin.js @@ -0,0 +1,34 @@ +export default class PagesPlugin { + apply (compiler) { + const isBundledPage = /^bundles[/\\]pages.*\.js$/ + const matchRouteName = /^bundles[/\\]pages[/\\](.*)\.js$/ + + compiler.plugin('after-compile', (compilation, callback) => { + const pages = Object + .keys(compilation.namedChunks) + .map(key => compilation.namedChunks[key]) + .filter(chunk => isBundledPage.test(chunk.name)) + + pages.forEach((chunk) => { + const page = compilation.assets[chunk.name] + const pageName = matchRouteName.exec(chunk.name)[1] + const routeName = `/${pageName.replace(/index$/, '')}` + + const content = page.source() + const newContent = ` + var comp = ${content} + NEXT_PAGE_LOADER.registerPage('${routeName}', null, comp.default) + ` + // Replace the current asset + // TODO: We need to move "client-bundles" back to "bundles" once we remove + // all the JSON eval stuff + delete compilation.assets[chunk.name] + compilation.assets[`client-bundles/pages/${pageName}.js`] = { + source: () => newContent, + size: () => newContent.length + } + }) + callback() + }) + } +} diff --git a/server/build/webpack.js b/server/build/webpack.js index 804434a32d..90564e3e28 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -7,6 +7,7 @@ import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin' import CaseSensitivePathPlugin from 'case-sensitive-paths-webpack-plugin' import UnlinkFilePlugin from './plugins/unlink-file-plugin' import JsonPagesPlugin from './plugins/json-pages-plugin' +import PagesPlugin from './plugins/pages-plugin' import CombineAssetsPlugin from './plugins/combine-assets-plugin' import getConfig from '../config' import * as babelCore from 'babel-core' @@ -117,6 +118,7 @@ export default async function createCompiler (dir, { dev = false, quiet = false, 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production') }), new JsonPagesPlugin(), + new PagesPlugin(), new CaseSensitivePathPlugin() ] diff --git a/server/index.js b/server/index.js index 17a45090cd..8b1a8e0376 100644 --- a/server/index.js +++ b/server/index.js @@ -13,7 +13,7 @@ import { } from './render' import Router from './router' import HotReloader from './hot-reloader' -import { resolveFromList } from './resolve' +import resolvePath, { resolveFromList } from './resolve' import getConfig from './config' // We need to go up one more level since we are in the `dist` directory import pkg from '../../package' @@ -127,6 +127,28 @@ export default class Server { await this.renderJSON(req, res, pathname) }, + '/_next/:buildId/page/:path*': async (req, res, params) => { + const paths = params.path || [''] + const pathname = `/${paths.join('/')}` + + await this.hotReloader.ensurePage(pathname) + + if (!this.handleBuildId(params.buildId, res)) { + res.setHeader('Content-Type', 'text/javascript') + // TODO: Handle buildId mismatches properly. + res.end(` + var error = new Error('INVALID_BUILD_ID') + error.buildIdMismatched = true + NEXT_PAGE_LOADER.registerPage('${pathname}', error) + `) + return + } + + const path = join(this.dir, '.next', 'client-bundles', 'pages', pathname) + const realPath = await resolvePath(path) + await this.serveStatic(req, res, realPath) + }, + '/_next/:path+': async (req, res, params) => { const p = join(__dirname, '..', 'client', ...(params.path || [])) await this.serveStatic(req, res, p) From 893884302507e01b1a44b3bedf06103cb142de51 Mon Sep 17 00:00:00 2001 From: Arunoda Susiripala Date: Mon, 3 Apr 2017 23:44:42 +0530 Subject: [PATCH 02/40] Call ensurePage only in the dev mode. --- server/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/index.js b/server/index.js index 8b1a8e0376..6b00f4b5a1 100644 --- a/server/index.js +++ b/server/index.js @@ -131,7 +131,9 @@ export default class Server { const paths = params.path || [''] const pathname = `/${paths.join('/')}` - await this.hotReloader.ensurePage(pathname) + if (this.dev) { + await this.hotReloader.ensurePage(pathname) + } if (!this.handleBuildId(params.buildId, res)) { res.setHeader('Content-Type', 'text/javascript') From c95d2b28d01f03cc0b0c386051f6236975d0d005 Mon Sep 17 00:00:00 2001 From: Arunoda Susiripala Date: Wed, 5 Apr 2017 01:25:56 +0530 Subject: [PATCH 03/40] Implement router using the page-loader. --- client/index.js | 5 +- lib/error.js | 11 +++-- lib/page-loader.js | 28 +++++++++-- lib/router/router.js | 113 +++++++++++++++++-------------------------- server/index.js | 53 +++++++++----------- server/render.js | 38 +++++++++++++++ 6 files changed, 138 insertions(+), 110 deletions(-) diff --git a/client/index.js b/client/index.js index 73e36f93a7..f6d2ffc786 100644 --- a/client/index.js +++ b/client/index.js @@ -61,7 +61,10 @@ export default () => { } export async function render (props) { - if (props.err) { + // There are some errors we should ignore. + // Next.js rendering logic knows how to handle them. + // These are specially 404 errors + if (props.err && !props.err.ignore) { await renderError(props.err) return } diff --git a/lib/error.js b/lib/error.js index 1b4890836d..02e75423ac 100644 --- a/lib/error.js +++ b/lib/error.js @@ -3,14 +3,15 @@ import HTTPStatus from 'http-status' import Head from './head' export default class Error extends React.Component { - static getInitialProps ({ res, jsonPageRes }) { - const statusCode = res ? res.statusCode : (jsonPageRes ? jsonPageRes.status : null) - return { statusCode } + static getInitialProps ({ res, err }) { + const statusCode = res ? res.statusCode : (err ? err.statusCode : null) + const pageNotFound = statusCode === 404 || (err ? err.pageNotFound : false) + return { statusCode, pageNotFound } } render () { - const { statusCode } = this.props - const title = statusCode === 404 + const { statusCode, pageNotFound } = this.props + const title = pageNotFound ? 'This page could not be found' : HTTPStatus[statusCode] || 'An unexpected error has occurred' diff --git a/lib/page-loader.js b/lib/page-loader.js index 6964d30ba8..f58f52f34b 100644 --- a/lib/page-loader.js +++ b/lib/page-loader.js @@ -10,15 +10,23 @@ export default class PageLoader { this.loadingRoutes = {} } - loadPage (route) { + normalizeRoute (route) { if (route[0] !== '/') { throw new Error('Route name should start with a "/"') } - route = route.replace(/index$/, '') + return route.replace(/index$/, '') + } - if (this.pageCache[route]) { - return Promise.resolve(this.pageCache[route]) + loadPage (route) { + route = this.normalizeRoute(route) + + const cachedPage = this.pageCache[route] + if (cachedPage) { + return new Promise((resolve, reject) => { + if (cachedPage.error) return reject(cachedPage.error) + return resolve(cachedPage.page) + }) } return new Promise((resolve, reject) => { @@ -43,6 +51,8 @@ export default class PageLoader { } loadScript (route) { + route = this.normalizeRoute(route) + const script = document.createElement('script') const url = `/_next/${encodeURIComponent(this.buildId)}/page${route}` script.src = url @@ -57,8 +67,16 @@ export default class PageLoader { // This method if called by the route code. registerPage (route, error, page) { + route = this.normalizeRoute(route) + // add the page to the cache - this.pageCache[route] = page + this.pageCache[route] = { error, page } this.registerEvents.emit(route, { error, page }) } + + clearCache (route) { + route = this.normalizeRoute(route) + delete this.pageCache[route] + delete this.loadingRoutes[route] + } } diff --git a/lib/router/router.js b/lib/router/router.js index ecf7ac7bb3..64910f3dbd 100644 --- a/lib/router/router.js +++ b/lib/router/router.js @@ -1,7 +1,7 @@ +/* global NEXT_PAGE_LOADER */ + import { parse, format } from 'url' import mitt from 'mitt' -import fetch from 'unfetch' -import evalScript from '../eval-script' import shallowEquals from '../shallow-equals' import PQueue from '../p-queue' import { loadGetInitialProps, getURL } from '../utils' @@ -15,10 +15,13 @@ export default class Router { this.route = toRoute(pathname) // set up the component cache (by route keys) - this.components = { [this.route]: { Component, err } } - - // contain a map of promise of fetch routes - this.fetchingRoutes = {} + this.components = {} + // We should not keep the cache, if there's an error + // Otherwise, this cause issues when when going back and + // come again to the errored page. + if (Component !== ErrorComponent) { + this.components[this.route] = { Component, err } + } // Handling Router Events this.events = mitt() @@ -77,7 +80,7 @@ export default class Router { async reload (route) { delete this.components[route] - delete this.fetchingRoutes[route] + NEXT_PAGE_LOADER.clearCache(route) if (route !== this.route) return @@ -186,11 +189,11 @@ export default class Router { try { routeInfo = this.components[route] if (!routeInfo) { - routeInfo = await this.fetchComponent(route, as) + routeInfo = { Component: await this.fetchComponent(route, as) } } - const { Component, err, jsonPageRes } = routeInfo - const ctx = { err, pathname, query, jsonPageRes } + const { Component } = routeInfo + const ctx = { pathname, query } routeInfo.props = await this.getInitialProps(Component, ctx) this.components[route] = routeInfo @@ -199,13 +202,27 @@ export default class Router { return { error: err } } + if (err.buildIdMismatched) { + // Now we need to reload the page or do the action asked by the user + _notifyBuildIdMismatch(as) + // We also need to cancel this current route change. + // We do it like this. + err.cancelled = true + return { error: err } + } + + if (err.pageNotFound) { + // Indicate main error display logic to + // ignore rendering this error as a runtime error. + err.ignore = true + } + const Component = this.ErrorComponent routeInfo = { Component, err } const ctx = { err, pathname, query } routeInfo.props = await this.getInitialProps(Component, ctx) routeInfo.error = err - console.error(err) } return routeInfo @@ -268,28 +285,7 @@ export default class Router { cancelled = true } - const jsonPageRes = await this.fetchRoute(route) - let jsonData - // We can call .json() only once for a response. - // That's why we need to keep a copy of data if we already parsed it. - if (jsonPageRes.data) { - jsonData = jsonPageRes.data - } else { - jsonData = jsonPageRes.data = await jsonPageRes.json() - } - - if (jsonData.buildIdMismatch) { - _notifyBuildIdMismatch(as) - - const error = Error('Abort due to BUILD_ID mismatch') - error.cancelled = true - throw error - } - - const newData = { - ...await loadComponent(jsonData), - jsonPageRes - } + const Component = await this.fetchRoute(route) if (cancelled) { const error = new Error(`Abort fetching component for route: "${route}"`) @@ -301,7 +297,7 @@ export default class Router { this.componentLoadCancel = null } - return newData + return Component } async getInitialProps (Component, ctx) { @@ -324,24 +320,22 @@ export default class Router { return props } - fetchRoute (route) { - let promise = this.fetchingRoutes[route] - if (!promise) { - promise = this.fetchingRoutes[route] = this.doFetchRoute(route) + async fetchRoute (route) { + // Wait for webpack to became idle if it's not. + // More info: https://github.com/zeit/next.js/pull/1511 + if (webpackModule && webpackModule.hot && webpackModule.hot.status() !== 'idle') { + await new Promise((resolve) => { + const check = (status) => { + if (status === 'idle') { + webpackModule.hot.removeStatusHandler(check) + resolve() + } + } + webpackModule.hot.status(check) + }) } - return promise - } - - doFetchRoute (route) { - const { buildId } = window.__NEXT_DATA__ - const url = `/_next/${encodeURIComponent(buildId)}/pages${route}` - - return fetch(url, { - method: 'GET', - credentials: 'same-origin', - headers: { 'Accept': 'application/json' } - }) + return await NEXT_PAGE_LOADER.loadPage(route) } abortComponentLoad (as) { @@ -365,22 +359,3 @@ export default class Router { function toRoute (path) { return path.replace(/\/$/, '') || '/' } - -async function loadComponent (jsonData) { - if (webpackModule && webpackModule.hot && webpackModule.hot.status() !== 'idle') { - await new Promise((resolve) => { - const check = (status) => { - if (status === 'idle') { - webpackModule.hot.removeStatusHandler(check) - resolve() - } - } - webpackModule.hot.status(check) - }) - } - - const module = evalScript(jsonData.component) - const Component = module.default || module - - return { Component, err: jsonData.err } -} diff --git a/server/index.js b/server/index.js index 6b00f4b5a1..c45712fafe 100644 --- a/server/index.js +++ b/server/index.js @@ -9,11 +9,13 @@ import { renderJSON, renderErrorJSON, sendHTML, - serveStatic + serveStatic, + renderScript, + renderScriptError } from './render' import Router from './router' import HotReloader from './hot-reloader' -import resolvePath, { resolveFromList } from './resolve' +import { resolveFromList } from './resolve' import getConfig from './config' // We need to go up one more level since we are in the `dist` directory import pkg from '../../package' @@ -114,41 +116,28 @@ export default class Server { await this.serveStatic(req, res, p) }, - '/_next/:buildId/pages/:path*': async (req, res, params) => { - if (!this.handleBuildId(params.buildId, res)) { - res.setHeader('Content-Type', 'application/json') - res.end(JSON.stringify({ buildIdMismatch: true })) - return - } - - const paths = params.path || ['index'] - const pathname = `/${paths.join('/')}` - - await this.renderJSON(req, res, pathname) - }, - '/_next/:buildId/page/:path*': async (req, res, params) => { const paths = params.path || [''] - const pathname = `/${paths.join('/')}` - - if (this.dev) { - await this.hotReloader.ensurePage(pathname) - } + const page = `/${paths.join('/')}` if (!this.handleBuildId(params.buildId, res)) { - res.setHeader('Content-Type', 'text/javascript') - // TODO: Handle buildId mismatches properly. - res.end(` - var error = new Error('INVALID_BUILD_ID') - error.buildIdMismatched = true - NEXT_PAGE_LOADER.registerPage('${pathname}', error) - `) + const error = new Error('INVALID_BUILD_ID') + const customFields = { buildIdMismatched: true } + + await renderScriptError(req, res, page, error, customFields, this.renderOpts) return } - const path = join(this.dir, '.next', 'client-bundles', 'pages', pathname) - const realPath = await resolvePath(path) - await this.serveStatic(req, res, realPath) + if (this.dev) { + const compilationErr = this.getCompilationError(page) + if (compilationErr) { + const customFields = { buildError: true } + await renderScriptError(req, res, page, compilationErr, customFields, this.renderOpts) + return + } + } + + await renderScript(req, res, page, this.renderOpts) }, '/_next/:path+': async (req, res, params) => { @@ -314,6 +303,10 @@ export default class Server { } } + serveScript (req, res, path) { + return serveStatic(req, res, path) + } + readBuildId () { const buildIdPath = join(this.dir, '.next', 'BUILD_ID') const buildId = fs.readFileSync(buildIdPath, 'utf8') diff --git a/server/render.js b/server/render.js index 8a39a69dcc..1c202e43f1 100644 --- a/server/render.js +++ b/server/render.js @@ -118,6 +118,44 @@ export async function renderJSON (req, res, page, { dir = process.cwd(), hotRelo return serveStatic(req, res, pagePath) } +export async function renderScript (req, res, page, opts) { + try { + if (opts.dev) { + await opts.hotReloader.ensurePage(page) + } + + const path = join(opts.dir, '.next', 'client-bundles', 'pages', page) + const realPath = await resolvePath(path) + await serveStatic(req, res, realPath) + } catch (err) { + if (err.code === 'ENOENT') { + res.setHeader('Content-Type', 'text/javascript') + res.end(` + var error = new Error('Page not exists: ${page}') + error.pageNotFound = true + error.statusCode = 404 + NEXT_PAGE_LOADER.registerPage('${page}', error) + `) + return + } + + throw err + } +} + +export async function renderScriptError (req, res, page, error, customFields, opts) { + res.setHeader('Content-Type', 'text/javascript') + const errorJson = { + ...errorToJSON(error), + ...customFields + } + + res.end(` + var error = ${JSON.stringify(errorJson)} + NEXT_PAGE_LOADER.registerPage('${page}', error) + `) +} + export async function renderErrorJSON (err, req, res, { dir = process.cwd(), dev = false } = {}) { const component = await readPage(join(dir, '.next', 'bundles', 'pages', '_error')) From e3d68ff401fc21f99757bde29355c577dab2847d Mon Sep 17 00:00:00 2001 From: Arunoda Susiripala Date: Wed, 5 Apr 2017 02:05:25 +0530 Subject: [PATCH 04/40] Fix a typo and remove unwanted code. --- server/index.js | 52 ++++++++---------------------------------------- server/render.js | 38 +++++++++++------------------------ 2 files changed, 20 insertions(+), 70 deletions(-) diff --git a/server/index.js b/server/index.js index c45712fafe..16a4b7f435 100644 --- a/server/index.js +++ b/server/index.js @@ -6,8 +6,6 @@ import http, { STATUS_CODES } from 'http' import { renderToHTML, renderErrorToHTML, - renderJSON, - renderErrorJSON, sendHTML, serveStatic, renderScript, @@ -124,16 +122,20 @@ export default class Server { const error = new Error('INVALID_BUILD_ID') const customFields = { buildIdMismatched: true } - await renderScriptError(req, res, page, error, customFields, this.renderOpts) - return + return await renderScriptError(req, res, page, error, customFields, this.renderOpts) } if (this.dev) { + try { + await this.hotReloader.ensurePage(page) + } catch (error) { + return await renderScriptError(req, res, page, error, {}, this.renderOpts) + } + const compilationErr = this.getCompilationError(page) if (compilationErr) { const customFields = { buildError: true } - await renderScriptError(req, res, page, compilationErr, customFields, this.renderOpts) - return + return await renderScriptError(req, res, page, compilationErr, customFields, this.renderOpts) } } @@ -257,40 +259,6 @@ export default class Server { return this.renderError(null, req, res, pathname, query) } - async renderJSON (req, res, page) { - if (this.dev) { - const compilationErr = this.getCompilationError(page) - if (compilationErr) { - return this.renderErrorJSON(compilationErr, req, res) - } - } - - try { - return await renderJSON(req, res, page, this.renderOpts) - } catch (err) { - if (err.code === 'ENOENT') { - res.statusCode = 404 - return this.renderErrorJSON(null, req, res) - } else { - if (!this.quiet) console.error(err) - res.statusCode = 500 - return this.renderErrorJSON(err, req, res) - } - } - } - - async renderErrorJSON (err, req, res) { - if (this.dev) { - const compilationErr = this.getCompilationError('/_error') - if (compilationErr) { - res.statusCode = 500 - return renderErrorJSON(compilationErr, req, res, this.renderOpts) - } - } - - return renderErrorJSON(err, req, res, this.renderOpts) - } - async serveStatic (req, res, path) { try { return await serveStatic(req, res, path) @@ -303,10 +271,6 @@ export default class Server { } } - serveScript (req, res, path) { - return serveStatic(req, res, path) - } - readBuildId () { const buildIdPath = join(this.dir, '.next', 'BUILD_ID') const buildId = fs.readFileSync(buildIdPath, 'utf8') diff --git a/server/render.js b/server/render.js index 1c202e43f1..f7ee665e0b 100644 --- a/server/render.js +++ b/server/render.js @@ -112,30 +112,14 @@ async function doRender (req, res, pathname, query, { return '' + renderToStaticMarkup(doc) } -export async function renderJSON (req, res, page, { dir = process.cwd(), hotReloader } = {}) { - await ensurePage(page, { dir, hotReloader }) - const pagePath = await resolvePath(join(dir, '.next', 'bundles', 'pages', page)) - return serveStatic(req, res, pagePath) -} - export async function renderScript (req, res, page, opts) { try { - if (opts.dev) { - await opts.hotReloader.ensurePage(page) - } - const path = join(opts.dir, '.next', 'client-bundles', 'pages', page) const realPath = await resolvePath(path) await serveStatic(req, res, realPath) } catch (err) { if (err.code === 'ENOENT') { - res.setHeader('Content-Type', 'text/javascript') - res.end(` - var error = new Error('Page not exists: ${page}') - error.pageNotFound = true - error.statusCode = 404 - NEXT_PAGE_LOADER.registerPage('${page}', error) - `) + renderScriptError(req, res, page, err, {}, opts) return } @@ -144,6 +128,17 @@ export async function renderScript (req, res, page, opts) { } export async function renderScriptError (req, res, page, error, customFields, opts) { + if (error.code === 'ENOENT') { + res.setHeader('Content-Type', 'text/javascript') + res.end(` + var error = new Error('Page not exists: ${page}') + error.pageNotFound = true + error.statusCode = 404 + NEXT_PAGE_LOADER.registerPage('${page}', error) + `) + return + } + res.setHeader('Content-Type', 'text/javascript') const errorJson = { ...errorToJSON(error), @@ -156,15 +151,6 @@ export async function renderScriptError (req, res, page, error, customFields, op `) } -export async function renderErrorJSON (err, req, res, { dir = process.cwd(), dev = false } = {}) { - const component = await readPage(join(dir, '.next', 'bundles', 'pages', '_error')) - - sendJSON(res, { - component, - err: err && dev ? errorToJSON(err) : null - }, req.method) -} - export function sendHTML (res, html, method) { if (res.finished) return From 03209d88fd014293e59dc40c9ebda5ae7b21ec6e Mon Sep 17 00:00:00 2001 From: Arunoda Susiripala Date: Wed, 5 Apr 2017 02:38:38 +0530 Subject: [PATCH 05/40] Fix some issues related to rendering. --- client/index.js | 4 ++-- lib/router/router.js | 9 ++++----- server/build/plugins/pages-plugin.js | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/client/index.js b/client/index.js index f6d2ffc786..15876fb604 100644 --- a/client/index.js +++ b/client/index.js @@ -31,13 +31,13 @@ const { location } = window -window.NEXT_PAGE_LOADER = new PageLoader(buildId) - +const pageLoader = window.NEXT_PAGE_LOADER = new PageLoader(buildId) const Component = evalScript(component).default const ErrorComponent = evalScript(errorComponent).default let lastAppProps export const router = createRouter(pathname, query, getURL(), { + pageLoader, Component, ErrorComponent, err diff --git a/lib/router/router.js b/lib/router/router.js index 64910f3dbd..ea70df6851 100644 --- a/lib/router/router.js +++ b/lib/router/router.js @@ -1,5 +1,3 @@ -/* global NEXT_PAGE_LOADER */ - import { parse, format } from 'url' import mitt from 'mitt' import shallowEquals from '../shallow-equals' @@ -10,7 +8,7 @@ import { _notifyBuildIdMismatch } from './' const webpackModule = module export default class Router { - constructor (pathname, query, as, { Component, ErrorComponent, err } = {}) { + constructor (pathname, query, as, { pageLoader, Component, ErrorComponent, err } = {}) { // represents the current component key this.route = toRoute(pathname) @@ -26,6 +24,7 @@ export default class Router { // Handling Router Events this.events = mitt() + this.pageLoader = pageLoader this.prefetchQueue = new PQueue({ concurrency: 2 }) this.ErrorComponent = ErrorComponent this.pathname = pathname @@ -80,7 +79,7 @@ export default class Router { async reload (route) { delete this.components[route] - NEXT_PAGE_LOADER.clearCache(route) + this.pageLoader.clearCache(route) if (route !== this.route) return @@ -335,7 +334,7 @@ export default class Router { }) } - return await NEXT_PAGE_LOADER.loadPage(route) + return await this.pageLoader.loadPage(route) } abortComponentLoad (as) { diff --git a/server/build/plugins/pages-plugin.js b/server/build/plugins/pages-plugin.js index c5dab55732..86b990a6cf 100644 --- a/server/build/plugins/pages-plugin.js +++ b/server/build/plugins/pages-plugin.js @@ -12,7 +12,7 @@ export default class PagesPlugin { pages.forEach((chunk) => { const page = compilation.assets[chunk.name] const pageName = matchRouteName.exec(chunk.name)[1] - const routeName = `/${pageName.replace(/index$/, '')}` + const routeName = `/${pageName.replace(/[/\\]index$/, '')}` const content = page.source() const newContent = ` From a1f11a4660f4f3f8b1ac09da9175390f23a168ad Mon Sep 17 00:00:00 2001 From: Arunoda Susiripala Date: Wed, 5 Apr 2017 08:18:15 +0530 Subject: [PATCH 06/40] Fix production tests. --- test/integration/production/pages/about.js | 3 ++ test/integration/production/pages/index.js | 7 +++- .../integration/production/test/index.test.js | 36 ++++++------------- 3 files changed, 19 insertions(+), 27 deletions(-) create mode 100644 test/integration/production/pages/about.js diff --git a/test/integration/production/pages/about.js b/test/integration/production/pages/about.js new file mode 100644 index 0000000000..870b2037ba --- /dev/null +++ b/test/integration/production/pages/about.js @@ -0,0 +1,3 @@ +export default () => ( +
About Page
+) diff --git a/test/integration/production/pages/index.js b/test/integration/production/pages/index.js index 3d446a4e89..e72a01d9d6 100644 --- a/test/integration/production/pages/index.js +++ b/test/integration/production/pages/index.js @@ -1,3 +1,8 @@ +import Link from 'next/link' + export default () => ( -
Hello World
+
+ About Page +

Hello World

+
) diff --git a/test/integration/production/test/index.test.js b/test/integration/production/test/index.test.js index 2cd043c228..b15c02eedb 100644 --- a/test/integration/production/test/index.test.js +++ b/test/integration/production/test/index.test.js @@ -1,6 +1,5 @@ /* global jasmine, describe, it, expect, beforeAll, afterAll */ -import fetch from 'node-fetch' import { join } from 'path' import { nextServer, @@ -9,6 +8,7 @@ import { stopApp, renderViaHTTP } from 'next-test-utils' +import webdriver from 'next-webdriver' const appDir = join(__dirname, '../') let appPort @@ -37,32 +37,16 @@ describe('Production Usage', () => { }) }) - describe('JSON pages', () => { - describe('when asked for a normal page', () => { - it('should serve the normal page', async () => { - const url = `http://localhost:${appPort}/_next/${app.renderOpts.buildId}/pages` - const res = await fetch(url, { compress: false }) - expect(res.headers.get('Content-Encoding')).toBeNull() + describe('With navigation', () => { + it('should navigate via client side', async () => { + const browser = await webdriver(appPort, '/') + const text = await browser + .elementByCss('a').click() + .waitForElementByCss('.about-page') + .elementByCss('div').text() - const page = await res.json() - expect(page.component).toBeDefined() - }) - }) - - describe('when asked for a page with an unknown encoding', () => { - it('should serve the normal page', async () => { - const url = `http://localhost:${appPort}/_next/${app.renderOpts.buildId}/pages` - const res = await fetch(url, { - compress: false, - headers: { - 'Accept-Encoding': 'br' - } - }) - expect(res.headers.get('Content-Encoding')).toBeNull() - - const page = await res.json() - expect(page.component).toBeDefined() - }) + expect(text).toBe('About Page') + browser.close() }) }) }) From 6f65a053a85b84f602beae95409199ae042b05ea Mon Sep 17 00:00:00 2001 From: Arunoda Susiripala Date: Wed, 5 Apr 2017 08:21:20 +0530 Subject: [PATCH 07/40] Fix ondemand test cases. --- test/integration/ondemand/test/index.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/ondemand/test/index.test.js b/test/integration/ondemand/test/index.test.js index 0726a8d446..7971f227be 100644 --- a/test/integration/ondemand/test/index.test.js +++ b/test/integration/ondemand/test/index.test.js @@ -31,13 +31,13 @@ describe('On Demand Entries', () => { }) it('should compile pages for JSON page requests', async () => { - const pageContent = await renderViaHTTP(context.appPort, '/_next/-/pages/about') + const pageContent = await renderViaHTTP(context.appPort, '/_next/-/page/about') expect(pageContent.includes('About Page')).toBeTruthy() }) it('should dispose inactive pages', async () => { await renderViaHTTP(context.appPort, '/_next/-/pages/about') - const aboutPagePath = resolve(__dirname, '../.next/bundles/pages/about.json') + const aboutPagePath = resolve(__dirname, '../.next/client-bundles/pages/about.js') expect(existsSync(aboutPagePath)).toBeTruthy() // Wait maximum of jasmine.DEFAULT_TIMEOUT_INTERVAL checking From 57e3a67f628719487317db8564a9c71742f936de Mon Sep 17 00:00:00 2001 From: Arunoda Susiripala Date: Wed, 5 Apr 2017 08:46:48 +0530 Subject: [PATCH 08/40] Fix unit tests. --- test/unit/router.test.js | 55 ++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/test/unit/router.test.js b/test/unit/router.test.js index 9a76ed04ce..294db9284a 100644 --- a/test/unit/router.test.js +++ b/test/unit/router.test.js @@ -1,52 +1,53 @@ /* global describe, it, expect */ import Router from '../../dist/lib/router/router' +class PageLoader { + constructor (options = {}) { + this.options = options + this.loaded = {} + } + + loadPage (route) { + this.loaded[route] = true + + if (this.options.delay) { + return new Promise((resolve) => setTimeout(resolve, this.options.delay)) + } + } +} + describe('Router', () => { const request = { clone: () => null } describe('.prefetch()', () => { it('should prefetch a given page', async () => { - const router = new Router('/', {}) - const promise = Promise.resolve(request) - const route = 'routex' - router.doFetchRoute = (r) => { - expect(r).toBe(route) - return promise - } + const pageLoader = new PageLoader() + const router = new Router('/', {}, '/', { pageLoader }) + const route = '/routex' await router.prefetch(route) - expect(router.fetchingRoutes[route]).toBe(promise) - }) - - it('should stop if it\'s prefetching already', async () => { - const router = new Router('/', {}) - const route = 'routex' - router.fetchingRoutes[route] = Promise.resolve(request) - router.doFetchRoute = () => { throw new Error('Should not happen') } - await router.prefetch(route) + expect(pageLoader.loaded['/routex']).toBeTruthy() }) it('should only run two jobs at a time', async () => { - const router = new Router('/', {}) - let count = 0 - - router.doFetchRoute = () => { - count++ - return new Promise((resolve) => {}) - } + // delay loading pages for an hour + const pageLoader = new PageLoader({ delay: 1000 * 3600 }) + const router = new Router('/', {}, '/', { pageLoader }) router.prefetch('route1') router.prefetch('route2') router.prefetch('route3') router.prefetch('route4') + // Wait for a bit await new Promise((resolve) => setTimeout(resolve, 50)) - expect(count).toBe(2) - expect(Object.keys(router.fetchingRoutes)).toEqual(['route1', 'route2']) + expect(Object.keys(pageLoader.loaded).length).toBe(2) + expect(Object.keys(pageLoader.loaded)).toEqual(['route1', 'route2']) }) it('should run all the jobs', async () => { - const router = new Router('/', {}) + const pageLoader = new PageLoader() + const router = new Router('/', {}, '/', { pageLoader }) const routes = ['route1', 'route2', 'route3', 'route4'] router.doFetchRoute = () => Promise.resolve(request) @@ -56,7 +57,7 @@ describe('Router', () => { await router.prefetch(routes[2]) await router.prefetch(routes[3]) - expect(Object.keys(router.fetchingRoutes)).toEqual(routes) + expect(Object.keys(pageLoader.loaded)).toEqual(routes) }) }) }) From 822a99b0d554648311ce341081041dca81ca78f7 Mon Sep 17 00:00:00 2001 From: Arunoda Susiripala Date: Wed, 5 Apr 2017 12:15:39 +0530 Subject: [PATCH 09/40] Get rid of eval completely. --- client/index.js | 12 +++++++----- lib/page-loader.js | 13 +++++++++++++ server/build/plugins/pages-plugin.js | 15 ++++++++++++--- server/document.js | 12 ++++++++++++ server/read-page.js | 6 ++---- server/render.js | 8 ++++---- 6 files changed, 50 insertions(+), 16 deletions(-) diff --git a/client/index.js b/client/index.js index 15876fb604..fc32fc6ebd 100644 --- a/client/index.js +++ b/client/index.js @@ -4,7 +4,6 @@ import mitt from 'mitt' import HeadManager from './head-manager' import { createRouter } from '../lib/router' import App from '../lib/app' -import evalScript from '../lib/eval-script' import { loadGetInitialProps, getURL } from '../lib/utils' import ErrorDebugComponent from '../lib/error-debug' import PageLoader from '../lib/page-loader' @@ -20,8 +19,6 @@ if (!window.Promise) { const { __NEXT_DATA__: { - component, - errorComponent, props, err, pathname, @@ -32,8 +29,13 @@ const { } = window const pageLoader = window.NEXT_PAGE_LOADER = new PageLoader(buildId) -const Component = evalScript(component).default -const ErrorComponent = evalScript(errorComponent).default +if (window.NEXT_LOADED_PAGES) { + window.NEXT_LOADED_PAGES.forEach((fn) => fn()) + delete window.NEXT_LOADED_PAGES +} + +const ErrorComponent = pageLoader.loadPageSync('/_error') +const Component = pageLoader.loadPageSync(pathname) || ErrorComponent let lastAppProps export const router = createRouter(pathname, query, getURL(), { diff --git a/lib/page-loader.js b/lib/page-loader.js index f58f52f34b..4b1b33a1a6 100644 --- a/lib/page-loader.js +++ b/lib/page-loader.js @@ -18,6 +18,19 @@ export default class PageLoader { return route.replace(/index$/, '') } + loadPageSync (route) { + route = this.normalizeRoute(route) + const cachedPage = this.pageCache[route] + + if (!cachedPage) { + return null + } else if (cachedPage.error) { + throw cachedPage.error + } else { + return cachedPage.page + } + } + loadPage (route) { route = this.normalizeRoute(route) diff --git a/server/build/plugins/pages-plugin.js b/server/build/plugins/pages-plugin.js index 86b990a6cf..d0fb96949a 100644 --- a/server/build/plugins/pages-plugin.js +++ b/server/build/plugins/pages-plugin.js @@ -12,12 +12,21 @@ export default class PagesPlugin { pages.forEach((chunk) => { const page = compilation.assets[chunk.name] const pageName = matchRouteName.exec(chunk.name)[1] - const routeName = `/${pageName.replace(/[/\\]index$/, '')}` + const routeName = `/${pageName.replace(/[/\\]?index$/, '')}` const content = page.source() const newContent = ` - var comp = ${content} - NEXT_PAGE_LOADER.registerPage('${routeName}', null, comp.default) + function loadPage () { + var comp = ${content} + window.NEXT_PAGE_LOADER.registerPage('${routeName}', null, comp.default) + } + + if (window.NEXT_PAGE_LOADER) { + loadPage() + } else { + window.NEXT_LOADED_PAGES = window.NEXT_LOADED_PAGES || [] + window.NEXT_LOADED_PAGES.push(loadPage) + } ` // Replace the current asset // TODO: We need to move "client-bundles" back to "bundles" once we remove diff --git a/server/document.js b/server/document.js index 6c698da29e..6bc923715c 100644 --- a/server/document.js +++ b/server/document.js @@ -95,6 +95,17 @@ export class NextScript extends Component { return this.getChunkScript('app.js', { async: true }) } + getMainComponents () { + const { component, errorComponent } = this.context._documentProps + + return ( +
+