From 6e0e2307dab46eeeda8fd761986f33350a811cee Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Thu, 6 Apr 2017 10:29:18 -0700 Subject: [PATCH 01/25] Release 2.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 309f9557..ed1643db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "2.0.1", + "version": "2.1.0", "description": "Minimalistic framework for server-rendered React applications", "main": "./dist/server/next.js", "license": "MIT", From 8d2bbf940d52c8f44709a62bcd6d46ca34171e2d Mon Sep 17 00:00:00 2001 From: alex newman Date: Fri, 7 Apr 2017 17:52:12 +0100 Subject: [PATCH 02/25] Refactor the build server to remove tie to fs (#1656) --- server/build/index.js | 19 +++++++------------ server/build/replace.js | 5 ++--- server/build/webpack.js | 2 +- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/server/build/index.js b/server/build/index.js index 95195460..e13d36ff 100644 --- a/server/build/index.js +++ b/server/build/index.js @@ -1,6 +1,5 @@ import { tmpdir } from 'os' import { join } from 'path' -import getConfig from '../config' import fs from 'mz/fs' import uuid from 'uuid' import del from 'del' @@ -14,10 +13,8 @@ export default async function build (dir) { try { await runCompiler(compiler) - - // Pass in both the buildDir and the dir to retrieve config - await writeBuildStats(buildDir, dir) - await writeBuildId(buildDir, dir) + await writeBuildStats(buildDir) + await writeBuildId(buildDir) } catch (err) { console.error(`> Failed to build on ${buildDir}`) throw err @@ -48,24 +45,22 @@ function runCompiler (compiler) { }) } -async function writeBuildStats (buildDir, dir) { - const dist = getConfig(dir).distDir +async function writeBuildStats (dir) { // Here we can't use hashes in webpack chunks. // That's because the "app.js" is not tied to a chunk. // It's created by merging a few assets. (commons.js and main.js) // So, we need to generate the hash ourself. const assetHashMap = { 'app.js': { - hash: await md5File(join(buildDir, dist, 'app.js')) + hash: await md5File(join(dir, '.next', 'app.js')) } } - const buildStatsPath = join(buildDir, dist, 'build-stats.json') + const buildStatsPath = join(dir, '.next', 'build-stats.json') await fs.writeFile(buildStatsPath, JSON.stringify(assetHashMap), 'utf8') } -async function writeBuildId (buildDir, dir) { - const dist = getConfig(dir).distDir - const buildIdPath = join(buildDir, dist, 'BUILD_ID') +async function writeBuildId (dir) { + const buildIdPath = join(dir, '.next', 'BUILD_ID') const buildId = uuid.v4() await fs.writeFile(buildIdPath, buildId, 'utf8') } diff --git a/server/build/replace.js b/server/build/replace.js index 1679ba25..22ff9e4a 100644 --- a/server/build/replace.js +++ b/server/build/replace.js @@ -4,10 +4,9 @@ import getConfig from '../config' export default async function replaceCurrentBuild (dir, buildDir) { const dist = getConfig(dir).distDir - const buildDist = getConfig(buildDir).distDir const _dir = join(dir, dist) - const _buildDir = join(buildDir, dist) - const oldDir = join(buildDir, `${buildDist}.old`) + const _buildDir = join(buildDir, '.next') + const oldDir = join(buildDir, '.next.old') try { await move(_dir, oldDir) diff --git a/server/build/webpack.js b/server/build/webpack.js index 5e9785ed..a4a4c7ae 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -265,7 +265,7 @@ export default async function createCompiler (dir, { dev = false, quiet = false, context: dir, entry, output: { - path: join(buildDir || dir, config.distDir), + path: buildDir ? join(buildDir, '.next') : join(dir, config.distDir), filename: '[name]', libraryTarget: 'commonjs2', publicPath: '/_webpack/', From 0b8a14f35ce21345fb9b4f840dc1ddf1cc06c6fe Mon Sep 17 00:00:00 2001 From: Arunoda Susiripala Date: Fri, 7 Apr 2017 22:22:41 +0530 Subject: [PATCH 03/25] Use existing route info when Router.reload() is called. (#1654) Earlier, it tries to get that info from the location.href. That's incorrect. --- lib/router/router.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/router/router.js b/lib/router/router.js index ecf7ac7b..cf3197c0 100644 --- a/lib/router/router.js +++ b/lib/router/router.js @@ -81,8 +81,8 @@ export default class Router { if (route !== this.route) return + const { pathname, query } = this const url = window.location.href - const { pathname, query } = parse(url, true) this.events.emit('routeChangeStart', url) const routeInfo = await this.getRouteInfo(route, pathname, query, url) From 5a48e8afa3f1f2d05a9e1d324e2fca7c56c8fa37 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 7 Apr 2017 09:58:44 -0700 Subject: [PATCH 04/25] fix(package): update babel-plugin-transform-react-remove-prop-types to version 0.3.3 (#1649) https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ed1643db..80ad7d90 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "babel-plugin-transform-es2015-modules-commonjs": "6.24.0", "babel-plugin-transform-object-rest-spread": "6.22.0", "babel-plugin-transform-react-jsx-source": "6.22.0", - "babel-plugin-transform-react-remove-prop-types": "0.3.2", + "babel-plugin-transform-react-remove-prop-types": "0.3.3", "babel-plugin-transform-runtime": "6.22.0", "babel-preset-latest": "6.24.0", "babel-preset-react": "6.23.0", From feb62816e008b460f976a05978c7d90b560fdd78 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 7 Apr 2017 10:00:43 -0700 Subject: [PATCH 05/25] chore(package): update chromedriver to version 2.29.0 (#1648) https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 80ad7d90..027adaf9 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "babel-preset-es2015": "6.24.0", "benchmark": "2.1.4", "cheerio": "0.22.0", - "chromedriver": "2.28.0", + "chromedriver": "2.29.0", "coveralls": "2.13.0", "cross-env": "4.0.0", "fly": "2.0.5", From 0487956c4738ed0c961bececedbdeeca540b5d72 Mon Sep 17 00:00:00 2001 From: Remy Sharp Date: Fri, 7 Apr 2017 18:01:40 +0100 Subject: [PATCH 06/25] docs: don't blow away existing query string (#1638) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: don't blow away existing query string See comments in diff - I ran across this and it took me a while to work out why my client side code worked, but the server didn't. It was because I didn't realise that `.render`'s 3rd arg was the query object, so it was losing the _actual_ query string. * chore: remove trailing spaces ¯\_(ツ)_/¯ I think! --- examples/parameterized-routing/server.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/parameterized-routing/server.js b/examples/parameterized-routing/server.js index 35a42482..1089d526 100644 --- a/examples/parameterized-routing/server.js +++ b/examples/parameterized-routing/server.js @@ -12,14 +12,16 @@ const match = route('/blog/:id') app.prepare() .then(() => { createServer((req, res) => { - const { pathname } = parse(req.url) + const { pathname, query } = parse(req.url, true) const params = match(pathname) if (params === false) { handle(req, res) return } - - app.render(req, res, '/blog', params) + // assigning `query` into the params means that we still + // get the query string passed to our application + // i.e. /blog/foo?show-comments=true + app.render(req, res, '/blog', Object.assign(params, query)) }) .listen(3000, (err) => { if (err) throw err From 0007cd2a970d098ed10f4d2039f7fcc7071e82cf Mon Sep 17 00:00:00 2001 From: Arunoda Susiripala Date: Fri, 7 Apr 2017 23:28:35 +0530 Subject: [PATCH 07/25] [custom server] Handle internal routes automatically (#1658) * Implement the initial version. * Improve the render logic a bit. * Move all the webpack paths under /_next/ * Keep the log:false flag. --- client/webpack-hot-middleware-client.js | 2 +- server/build/webpack.js | 2 +- server/hot-reloader.js | 7 ++- server/index.js | 59 +++++++++++++++++-------- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/client/webpack-hot-middleware-client.js b/client/webpack-hot-middleware-client.js index 7dfc4949..6f1ea9e6 100644 --- a/client/webpack-hot-middleware-client.js +++ b/client/webpack-hot-middleware-client.js @@ -1,4 +1,4 @@ -import webpackHotMiddlewareClient from 'webpack-hot-middleware/client?overlay=false&reload=true' +import webpackHotMiddlewareClient from 'webpack-hot-middleware/client?overlay=false&reload=true&path=/_next/webpack-hmr' import Router from '../lib/router' const handlers = { diff --git a/server/build/webpack.js b/server/build/webpack.js index a4a4c7ae..6c7ffe36 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -268,7 +268,7 @@ export default async function createCompiler (dir, { dev = false, quiet = false, path: buildDir ? join(buildDir, '.next') : join(dir, config.distDir), filename: '[name]', libraryTarget: 'commonjs2', - publicPath: '/_webpack/', + publicPath: '/_next/webpack/', strictModuleExceptionHandling: true, devtoolModuleFilenameTemplate ({ resourcePath }) { const hash = createHash('sha1') diff --git a/server/hot-reloader.js b/server/hot-reloader.js index 15998aab..7ad4c710 100644 --- a/server/hot-reloader.js +++ b/server/hot-reloader.js @@ -140,7 +140,7 @@ export default class HotReloader { } : {} this.webpackDevMiddleware = webpackDevMiddleware(compiler, { - publicPath: '/_webpack/', + publicPath: '/_next/webpack/', noInfo: true, quiet: true, clientLogLevel: 'warning', @@ -148,7 +148,10 @@ export default class HotReloader { ...windowsSettings }) - this.webpackHotMiddleware = webpackHotMiddleware(compiler, { log: false }) + this.webpackHotMiddleware = webpackHotMiddleware(compiler, { + path: '/_next/webpack-hmr', + log: false + }) this.onDemandEntries = onDemandEntryHandler(this.webpackDevMiddleware, compiler, { dir: this.dir, dev: true, diff --git a/server/index.js b/server/index.js index 75e4e7bf..c54f82d8 100644 --- a/server/index.js +++ b/server/index.js @@ -18,6 +18,11 @@ import getConfig from './config' // We need to go up one more level since we are in the `dist` directory import pkg from '../../package' +const internalPrefixes = [ + /^\/_next\//, + /^\/static\// +] + export default class Server { constructor ({ dir = '.', dev = false, staticMarkup = false, quiet = false } = {}) { this.dir = resolve(dir) @@ -42,25 +47,27 @@ export default class Server { this.defineRoutes() } - getRequestHandler () { - return (req, res, parsedUrl) => { - // Parse url if parsedUrl not provided - if (!parsedUrl) { - parsedUrl = parseUrl(req.url, true) - } - - // Parse the querystring ourselves if the user doesn't handle querystring parsing - if (typeof parsedUrl.query === 'string') { - parsedUrl.query = parseQs(parsedUrl.query) - } - - return this.run(req, res, parsedUrl) - .catch((err) => { - if (!this.quiet) console.error(err) - res.statusCode = 500 - res.end(STATUS_CODES[500]) - }) + handleRequest (req, res, parsedUrl) { + // Parse url if parsedUrl not provided + if (!parsedUrl) { + parsedUrl = parseUrl(req.url, true) } + + // Parse the querystring ourselves if the user doesn't handle querystring parsing + if (typeof parsedUrl.query === 'string') { + parsedUrl.query = parseQs(parsedUrl.query) + } + + return this.run(req, res, parsedUrl) + .catch((err) => { + if (!this.quiet) console.error(err) + res.statusCode = 500 + res.end(STATUS_CODES[500]) + }) + } + + getRequestHandler () { + return this.handleRequest.bind(this) } async prepare () { @@ -181,7 +188,11 @@ export default class Server { } } - async render (req, res, pathname, query) { + async render (req, res, pathname, query, parsedUrl) { + if (this.isInternalUrl(req)) { + return this.handleRequest(req, res, parsedUrl) + } + if (this.config.poweredByHeader) { res.setHeader('X-Powered-By', `Next.js ${pkg.version}`) } @@ -291,6 +302,16 @@ export default class Server { } } + isInternalUrl (req) { + for (const prefix of internalPrefixes) { + if (prefix.test(req.url)) { + return true + } + } + + return false + } + readBuildId () { const buildIdPath = join(this.dir, this.dist, 'BUILD_ID') const buildId = fs.readFileSync(buildIdPath, 'utf8') From 060cac3e3f43812f566df4efcc9ffe0fc8f77703 Mon Sep 17 00:00:00 2001 From: Arunoda Susiripala Date: Sat, 8 Apr 2017 00:09:00 +0530 Subject: [PATCH 08/25] Move all the modules used in 1/2 of all pages into the common chunks. (#1659) --- server/build/webpack.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/server/build/webpack.js b/server/build/webpack.js index 6c7ffe36..7f950593 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -36,7 +36,7 @@ export default async function createCompiler (dir, { dev = false, quiet = false, const mainJS = dev ? require.resolve('../../client/next-dev') : require.resolve('../../client/next') - let minChunks + let totalPages const entry = async () => { const entries = { @@ -68,8 +68,7 @@ export default async function createCompiler (dir, { dev = false, quiet = false, } } - // calculate minChunks of CommonsChunkPlugin for later use - minChunks = Math.max(2, pages.filter((p) => p !== documentPage).length) + totalPages = pages.filter((p) => p !== documentPage).length return entries } @@ -101,9 +100,8 @@ export default async function createCompiler (dir, { dev = false, quiet = false, return module.context && module.context.indexOf('node_modules') >= 0 } - // NOTE: it depends on the fact that the entry funtion is always called - // before applying CommonsChunkPlugin - return count >= minChunks + // Move modules used in at-least 1/2 of the total pages into commons. + return count >= totalPages * 0.5 } }), // This chunk contains all the webpack related code. So, all the changes From e4a42977a88dbf4e995225cc5d33c56efd2415c7 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 7 Apr 2017 11:40:57 -0700 Subject: [PATCH 09/25] Release 2.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 027adaf9..532d3ee1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "2.1.0", + "version": "2.1.1", "description": "Minimalistic framework for server-rendered React applications", "main": "./dist/server/next.js", "license": "MIT", From 0ed3978a5196486c3d32a0d84477a17559ae1f92 Mon Sep 17 00:00:00 2001 From: Guillermo Rauch Date: Fri, 7 Apr 2017 11:41:43 -0700 Subject: [PATCH 10/25] add lock --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7b397e2c..1bed96f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -759,9 +759,9 @@ babel-plugin-transform-react-jsx@^6.23.0: babel-plugin-syntax-jsx "^6.8.0" babel-runtime "^6.22.0" -babel-plugin-transform-react-remove-prop-types@0.3.2: - version "0.3.2" - resolved "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.3.2.tgz#6da8d834c6d7ad8ab02f956509790cfaa01ffe19" +babel-plugin-transform-react-remove-prop-types@0.3.3: + version "0.3.3" + resolved "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.3.3.tgz#d09f48b9a9503a69c1ced39b3825c2f45d29333c" babel-plugin-transform-regenerator@^6.22.0: version "6.22.0" @@ -1205,9 +1205,9 @@ chokidar@^1.4.3, chokidar@^1.6.1: optionalDependencies: fsevents "^1.0.0" -chromedriver@2.28.0: - version "2.28.0" - resolved "https://registry.yarnpkg.com/chromedriver/-/chromedriver-2.28.0.tgz#ea0c383621dd27db340c612b85fe39414c16ec79" +chromedriver@2.29.0: + version "2.29.0" + resolved "https://registry.npmjs.org/chromedriver/-/chromedriver-2.29.0.tgz#e3fd8b3c08dce2562b80ef1b0b846597659d0cc3" dependencies: adm-zip "^0.4.7" kew "^0.7.0" @@ -4023,11 +4023,11 @@ public-encrypt@^4.0.0: parse-asn1 "^5.0.0" randombytes "^2.0.1" -punycode@1.3.2: +punycode@1.3.2, punycode@^1.2.4: version "1.3.2" resolved "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" -punycode@^1.2.4, punycode@^1.4.1: +punycode@^1.4.1: version "1.4.1" resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" From c9bfba686543c83aff2566495b21e743db8817b2 Mon Sep 17 00:00:00 2001 From: Kevin Donahue Date: Mon, 10 Apr 2017 10:07:38 -0400 Subject: [PATCH 11/25] Only watch config file that exists on the level of source files (#1643) --- bin/next | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/next b/bin/next index 8654d788..79f635b7 100755 --- a/bin/next +++ b/bin/next @@ -1,9 +1,10 @@ #!/usr/bin/env node -import { join } from 'path' +import { join, resolve } from 'path' import { spawn } from 'cross-spawn' import { watchFile } from 'fs' import pkg from '../../package.json' +import getConfig from '../server/config' if (pkg.peerDependencies) { Object.keys(pkg.peerDependencies).forEach(dependency => { @@ -78,9 +79,10 @@ const startProcess = () => { } let proc = startProcess() +const { pagesDirectory = resolve(process.cwd(), 'pages') } = getConfig(process.cwd()) if (cmd === 'dev') { - watchFile(join(process.cwd(), 'next.config.js'), (cur, prev) => { + watchFile(`${resolve(pagesDirectory, '..')}/next.config.js`, (cur, prev) => { if (cur.size > 0 || prev.size > 0) { console.log('\n> Found a change in next.config.js, restarting the server...') // Don't listen to 'close' now since otherwise parent gets killed by listener From 8e6615dcf925a2bfb67e79c0b68e55b86670643d Mon Sep 17 00:00:00 2001 From: "C. T. Lin" Date: Tue, 11 Apr 2017 02:35:26 +0800 Subject: [PATCH 12/25] upgrade react to v15.5 and use prop-types instead of React.PropTypes (#1684) * upgrade react to v15.5 and use prop-types instead of React.PropTypes * Update package.json --- examples/with-pretty-url-routing/package.json | 1 + .../with-pretty-url-routing/pages/greeting.js | 5 ++-- lib/app.js | 3 +- lib/head.js | 3 +- lib/link.js | 3 +- package.json | 5 ++-- server/document.js | 3 +- yarn.lock | 30 ++++++++++++------- 8 files changed, 34 insertions(+), 19 deletions(-) diff --git a/examples/with-pretty-url-routing/package.json b/examples/with-pretty-url-routing/package.json index 6ecb3fb5..3080fbb7 100644 --- a/examples/with-pretty-url-routing/package.json +++ b/examples/with-pretty-url-routing/package.json @@ -8,6 +8,7 @@ "express": "^4.15.2", "next": "^2.0.0", "next-url-prettifier": "^1.0.2", + "prop-types": "^15.5.6", "react": "^15.4.2", "react-dom": "^15.4.2" } diff --git a/examples/with-pretty-url-routing/pages/greeting.js b/examples/with-pretty-url-routing/pages/greeting.js index e4fe354f..03b9d4fb 100644 --- a/examples/with-pretty-url-routing/pages/greeting.js +++ b/examples/with-pretty-url-routing/pages/greeting.js @@ -1,4 +1,5 @@ import React from 'react' +import PropTypes from 'prop-types' import {Link} from 'next-url-prettifier' import {Router} from '../routes' @@ -29,6 +30,6 @@ export default class GreetingPage extends React.Component { } GreetingPage.propTypes = { - lang: React.PropTypes.string, - name: React.PropTypes.string + lang: PropTypes.string, + name: PropTypes.string } diff --git a/lib/app.js b/lib/app.js index 2cc4d575..f8d33cee 100644 --- a/lib/app.js +++ b/lib/app.js @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import { AppContainer } from 'react-hot-loader' import shallowEquals from './shallow-equals' import { warn } from './utils' diff --git a/lib/head.js b/lib/head.js index ea580942..6200efe4 100644 --- a/lib/head.js +++ b/lib/head.js @@ -1,9 +1,10 @@ import React from 'react' +import PropTypes from 'prop-types' import sideEffect from './side-effect' class Head extends React.Component { static contextTypes = { - headManager: React.PropTypes.object + headManager: PropTypes.object } render () { diff --git a/lib/link.js b/lib/link.js index 87c4c32f..547161e7 100644 --- a/lib/link.js +++ b/lib/link.js @@ -1,5 +1,6 @@ import { resolve, format, parse } from 'url' -import React, { Component, Children, PropTypes } from 'react' +import React, { Component, Children } from 'react' +import PropTypes from 'prop-types' import Router from './router' import { warn, execOnce, getLocationOrigin } from './utils' diff --git a/package.json b/package.json index 532d3ee1..cc450d2a 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "mz": "2.6.0", "path-match": "1.2.4", "pkg-up": "1.0.0", + "prop-types": "15.5.6", "react-hot-loader": "3.0.0-beta.6", "send": "0.15.1", "source-map-support": "0.4.14", @@ -116,8 +117,8 @@ "node-fetch": "1.6.3", "node-notifier": "5.1.2", "nyc": "10.2.0", - "react": "15.4.2", - "react-dom": "15.4.2", + "react": "15.5.3", + "react-dom": "15.5.3", "standard": "9.0.2", "wd": "1.2.0" }, diff --git a/server/document.js b/server/document.js index 6c698da2..469c1223 100644 --- a/server/document.js +++ b/server/document.js @@ -1,4 +1,5 @@ -import React, { Component, PropTypes } from 'react' +import React, { Component } from 'react' +import PropTypes from 'prop-types' import htmlescape from 'htmlescape' import flush from 'styled-jsx/server' diff --git a/yarn.lock b/yarn.lock index 1bed96f9..1c6c5054 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2029,9 +2029,9 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" -fbjs@^0.8.1, fbjs@^0.8.4: - version "0.8.9" - resolved "https://registry.npmjs.org/fbjs/-/fbjs-0.8.9.tgz#180247fbd347dcc9004517b904f865400a0c8f14" +fbjs@^0.8.9: + version "0.8.12" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.12.tgz#10b5d92f76d45575fd63a217d4ea02bea2f8ed04" dependencies: core-js "^1.0.0" isomorphic-fetch "^2.1.1" @@ -4005,6 +4005,12 @@ promise@^7.1.1: dependencies: asap "~2.0.3" +prop-types@^15.5.2, prop-types@^15.5.6, prop-types@~15.5.0: + version "15.5.6" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.6.tgz#797a915b1714b645ebb7c5d6cc690346205bd2aa" + dependencies: + fbjs "^0.8.9" + prr@~0.0.0: version "0.0.0" resolved "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" @@ -4075,13 +4081,14 @@ react-deep-force-update@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/react-deep-force-update/-/react-deep-force-update-2.0.1.tgz#4f7f6c12c3e7de42f345992a3c518236fa1ecad3" -react-dom@15.4.2: - version "15.4.2" - resolved "https://registry.npmjs.org/react-dom/-/react-dom-15.4.2.tgz#015363f05b0a1fd52ae9efdd3a0060d90695208f" +react-dom@15.5.3: + version "15.5.3" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.5.3.tgz#2ee127ce942df55da53111ae303316e68072b5c5" dependencies: - fbjs "^0.8.1" + fbjs "^0.8.9" loose-envify "^1.1.0" object-assign "^4.1.0" + prop-types "~15.5.0" react-hot-loader@3.0.0-beta.6: version "3.0.0-beta.6" @@ -4100,13 +4107,14 @@ react-proxy@^3.0.0-alpha.0: dependencies: lodash "^4.6.1" -react@15.4.2: - version "15.4.2" - resolved "https://registry.npmjs.org/react/-/react-15.4.2.tgz#41f7991b26185392ba9bae96c8889e7e018397ef" +react@15.5.3: + version "15.5.3" + resolved "https://registry.yarnpkg.com/react/-/react-15.5.3.tgz#84055382c025dec4e3b902bb61a8697cc79c1258" dependencies: - fbjs "^0.8.4" + fbjs "^0.8.9" loose-envify "^1.1.0" object-assign "^4.1.0" + prop-types "^15.5.2" read-pkg-up@^1.0.1: version "1.0.1" From 2337827c40d4d0e866fe72067b2c92ff4cbc5870 Mon Sep 17 00:00:00 2001 From: Leo Lamprecht Date: Tue, 11 Apr 2017 13:07:00 +0200 Subject: [PATCH 13/25] We are not in beta anymore, so remove `publishConfig` Added in 629051db2cc64c01057ce253f5178c8e12fd8c4f --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index cc450d2a..6ef1f580 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,6 @@ "main": "./dist/server/next.js", "license": "MIT", "repository": "zeit/next.js", - "publishConfig": { - "tag": "beta" - }, "files": [ "dist", "babel.js", From 4df6f5b43480119240ac33a7fc8955e96e10b990 Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Thu, 13 Apr 2017 06:37:06 +0530 Subject: [PATCH 14/25] fix(package): update prop-types to version 15.5.7 (#1695) https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6ef1f580..83d648f5 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "mz": "2.6.0", "path-match": "1.2.4", "pkg-up": "1.0.0", - "prop-types": "15.5.6", + "prop-types": "15.5.7", "react-hot-loader": "3.0.0-beta.6", "send": "0.15.1", "source-map-support": "0.4.14", From af1afa472be7e8ab15711c84545a92c2a07d223b Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Thu, 13 Apr 2017 06:43:27 +0530 Subject: [PATCH 15/25] chore(package): update babel-eslint to version 7.2.2 (#1703) https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 83d648f5..671b990f 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "write-file-webpack-plugin": "4.0.0" }, "devDependencies": { - "babel-eslint": "7.2.0", + "babel-eslint": "7.2.2", "babel-jest": "18.0.0", "babel-plugin-istanbul": "4.1.1", "babel-plugin-transform-remove-strict-mode": "0.0.2", From a52458181a4e2f3d6e2fea64e34f158e087037f6 Mon Sep 17 00:00:00 2001 From: Jaga Santagostino Date: Thu, 13 Apr 2017 16:43:06 +0200 Subject: [PATCH 16/25] Add learnnextjs link (#1710) * Add learnnextjs link * fix typos --- readme.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/readme.md b/readme.md index cadc8e78..5d9ea7d8 100644 --- a/readme.md +++ b/readme.md @@ -11,6 +11,7 @@ Next.js is a minimalistic framework for server-rendered React applications. - [How to use](#how-to-use) + - [Getting Started](#getting-started) - [Setup](#setup) - [Automatic code splitting](#automatic-code-splitting) - [CSS](#css) @@ -43,6 +44,9 @@ Next.js is a minimalistic framework for server-rendered React applications. ## How to use +### Gettin Started +A step by step interactive guide of next features is available at [learnnextjs.com](https://learnnextjs.com/) + ### Setup Install it: From e0544300261bd6048089f887fdac3ff382da3966 Mon Sep 17 00:00:00 2001 From: Arunoda Susiripala Date: Thu, 13 Apr 2017 20:32:05 +0530 Subject: [PATCH 17/25] Fix a typo --- readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 5d9ea7d8..8225fe0f 100644 --- a/readme.md +++ b/readme.md @@ -44,7 +44,8 @@ Next.js is a minimalistic framework for server-rendered React applications. ## How to use -### Gettin Started +### Getting Started + A step by step interactive guide of next features is available at [learnnextjs.com](https://learnnextjs.com/) ### Setup From 2103e0541b8e2149a8b2e1e53d6a0ea8a66efb46 Mon Sep 17 00:00:00 2001 From: Jaga Santagostino Date: Thu, 13 Apr 2017 17:13:30 +0200 Subject: [PATCH 18/25] remove roadmap to 2.0 (#1711) 2.0 is already released --- readme.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/readme.md b/readme.md index 8225fe0f..0aad4737 100644 --- a/readme.md +++ b/readme.md @@ -36,7 +36,6 @@ Next.js is a minimalistic framework for server-rendered React applications. - [Customizing babel config](#customizing-babel-config) - [Production deployment](#production-deployment) - [FAQ](#faq) -- [Roadmap](#roadmap) - [Contributing](#contributing) - [Authors](#authors) @@ -845,10 +844,6 @@ As we were researching options for server-rendering React that didn’t involve -## Roadmap - -Our Roadmap towards 2.0.0 [is public](https://github.com/zeit/next.js/wiki/Roadmap#nextjs-200). - ## Contributing Please see our [contributing.md](./contributing.md) From b32f5e38147871051a28ad4717580b555114b9da Mon Sep 17 00:00:00 2001 From: "greenkeeper[bot]" Date: Fri, 14 Apr 2017 02:53:52 +0530 Subject: [PATCH 19/25] fix(package): update babel-plugin-transform-react-remove-prop-types to version 0.4.0 (#1713) https://greenkeeper.io/ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 671b990f..da5a2d84 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "babel-plugin-transform-es2015-modules-commonjs": "6.24.0", "babel-plugin-transform-object-rest-spread": "6.22.0", "babel-plugin-transform-react-jsx-source": "6.22.0", - "babel-plugin-transform-react-remove-prop-types": "0.3.3", + "babel-plugin-transform-react-remove-prop-types": "0.4.0", "babel-plugin-transform-runtime": "6.22.0", "babel-preset-latest": "6.24.0", "babel-preset-react": "6.23.0", From b46502f8f13172a6cc11167e88ee01369c582e96 Mon Sep 17 00:00:00 2001 From: Orlin M Bozhinov Date: Fri, 14 Apr 2017 23:54:55 +0300 Subject: [PATCH 20/25] corrected css pre-processing #readme link (#1709) --- examples/with-global-stylesheet/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/with-global-stylesheet/README.md b/examples/with-global-stylesheet/README.md index 2576c371..53cd30b8 100644 --- a/examples/with-global-stylesheet/README.md +++ b/examples/with-global-stylesheet/README.md @@ -37,7 +37,7 @@ Another babel plugin [module-resolver](https://github.com/tleunen/babel-plugin-m The `sass-loader` is configured with `includePaths: ['styles', 'node_modules']` so that your scss can `@import` from those places, again without relative paths, for maximum convenience and ability to use npm-published libraries. Furthermore, `glob` paths are also supported, so one could for example add `'node_modules/@material/*'` to the `includePaths`, which would make [material-components-web](https://github.com/material-components/material-components-web) (if you'd like) even easier to work with. -Furthermore, PostCSS is used to [pre-process](https://blog.madewithenvy.com/webpack-2-postcss-cssnext-fdcd2fd7d0bd#.r6t2d0smy) both `css` and `scss` stylesheets, the latter after Sass pre-processing. This is to illustrate `@import 'normalize.css';` from `node_modules` thanks to `postcss-easy-import`. [Autoprefixer](https://github.com/postcss/autoprefixer) is also added as a "best practice". Consider [cssnext](http://cssnext.io) instead, which includes `autoprefixer` as well as many other CSS spec features. +Furthermore, PostCSS is used to [pre-process](https://medium.com/@ddprrt/deconfusing-pre-and-post-processing-d68e3bd078a3) both `css` and `scss` stylesheets, the latter after Sass pre-processing. This is to illustrate `@import 'normalize.css';` from `node_modules` thanks to `postcss-easy-import`. [Autoprefixer](https://github.com/postcss/autoprefixer) is also added as a "best practice". Consider [cssnext](http://cssnext.io) instead, which includes `autoprefixer` as well as many other CSS spec features. This project shows how you can set it up. Have a look at: - .babelrc From bdc30bc0894eb10729d99762131a109e281d5b47 Mon Sep 17 00:00:00 2001 From: Xavier Brown Date: Sun, 16 Apr 2017 22:30:12 -0500 Subject: [PATCH 21/25] Update readme.md (#1741) The link element in the example code didn't end its first bracket before --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 0aad4737..7a731a54 100644 --- a/readme.md +++ b/readme.md @@ -302,7 +302,7 @@ The component `` can also receive an URL object and it will automatically // pages/index.js import Link from 'next/link' export default () => ( -
Click here to read more
+
Click here to read more
) ``` From dec85fe6c4c5360aced57d75f9cea78ed388f2f8 Mon Sep 17 00:00:00 2001 From: Arunoda Susiripala Date: Tue, 18 Apr 2017 09:48:43 +0530 Subject: [PATCH 22/25] Add CDN support with assetPrefix (#1700) * Introduce script tag based page loading system. * Call ensurePage only in the dev mode. * Implement router using the page-loader. * Fix a typo and remove unwanted code. * Fix some issues related to rendering. * Fix production tests. * Fix ondemand test cases. * Fix unit tests. * Get rid of eval completely. * Remove all the inline code. * Remove the json-pages plugin. * Rename NEXT_PAGE_LOADER into __NEXT_PAGE_LOADER__ * Rename NEXT_LOADED_PAGES into __NEXT_LOADED_PAGES__ * Remove some unwanted code. * Load everything async. * Remove lib/eval-script.js We no longer need it. * Move webpack idle wait code to the page-loader. Because that's the place to do it. * Remove pageNotFound key from the error. * Remove unused error field 'buildError' * Add much better logic to normalize routes. * Get rid of mitt. * Introduce a better way to register pages. * Came back to the mitt() based page-loader. * Add link rel=preload support. * Add assetPrefix support to add support for CDNs. * Add assetPrefix support for preload links. * Update readme.md --- client/index.js | 54 ++++++--- client/next-dev.js | 62 +++++----- client/next.js | 3 + client/on-demand-entries-client.js | 56 ++++----- client/webpack-hot-middleware-client.js | 76 +++++++------ lib/error.js | 4 +- lib/eval-script.js | 18 --- lib/page-loader.js | 102 +++++++++++++++++ lib/router/router.js | 106 ++++++------------ readme.md | 15 +++ server/build/plugins/json-pages-plugin.js | 24 ---- server/build/plugins/pages-plugin.js | 33 ++++++ server/build/webpack.js | 4 +- server/config.js | 3 +- server/document.js | 55 ++++++++- server/hot-reloader.js | 2 - server/index.js | 84 +++++++------- server/read-page.js | 25 ----- server/render.js | 66 ++++++----- test/integration/ondemand/test/index.test.js | 4 +- test/integration/production/pages/about.js | 3 + test/integration/production/pages/index.js | 7 +- .../integration/production/test/index.test.js | 36 ++---- test/unit/router.test.js | 55 ++++----- 24 files changed, 510 insertions(+), 387 deletions(-) delete mode 100644 lib/eval-script.js create mode 100644 lib/page-loader.js delete mode 100644 server/build/plugins/json-pages-plugin.js create mode 100644 server/build/plugins/pages-plugin.js delete mode 100644 server/read-page.js create mode 100644 test/integration/production/pages/about.js diff --git a/client/index.js b/client/index.js index 4d5c847a..55f9e052 100644 --- a/client/index.js +++ b/client/index.js @@ -4,9 +4,9 @@ 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' // Polyfill Promise globally // This is needed because Webpack2's dynamic loading(common chunks) code @@ -19,31 +19,50 @@ if (!window.Promise) { const { __NEXT_DATA__: { - component, - errorComponent, props, err, pathname, - query + query, + buildId, + assetPrefix }, location } = window -const Component = evalScript(component).default -const ErrorComponent = evalScript(errorComponent).default -let lastAppProps - -export const router = createRouter(pathname, query, getURL(), { - Component, - ErrorComponent, - err +const pageLoader = new PageLoader(buildId, assetPrefix) +window.__NEXT_LOADED_PAGES__.forEach(({ route, fn }) => { + pageLoader.registerPage(route, fn) }) +delete window.__NEXT_LOADED_PAGES__ + +window.__NEXT_REGISTER_PAGE = pageLoader.registerPage.bind(pageLoader) const headManager = new HeadManager() const appContainer = document.getElementById('__next') const errorContainer = document.getElementById('__next-error') -export default () => { +let lastAppProps +export let router +export let ErrorComponent +let Component + +export default async () => { + ErrorComponent = await pageLoader.loadPage('/_error') + + try { + Component = await pageLoader.loadPage(pathname) + } catch (err) { + console.error(`${err.message}\n${err.stack}`) + Component = ErrorComponent + } + + router = createRouter(pathname, query, getURL(), { + pageLoader, + Component, + ErrorComponent, + err + }) + const emitter = mitt() router.subscribe(({ Component, props, hash, err }) => { @@ -57,7 +76,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 } @@ -103,7 +125,7 @@ async function doRender ({ Component, props, hash, err, emitter }) { } if (emitter) { - emitter.emit('before-reactdom-render', { Component }) + emitter.emit('before-reactdom-render', { Component, ErrorComponent }) } Component = Component || lastAppProps.Component @@ -118,6 +140,6 @@ async function doRender ({ Component, props, hash, err, emitter }) { ReactDOM.render(createElement(App, appProps), appContainer) if (emitter) { - emitter.emit('after-reactdom-render', { Component }) + emitter.emit('after-reactdom-render', { Component, ErrorComponent }) } } diff --git a/client/next-dev.js b/client/next-dev.js index 071ec03d..b25f4169 100644 --- a/client/next-dev.js +++ b/client/next-dev.js @@ -1,14 +1,40 @@ -import evalScript from '../lib/eval-script' +import 'react-hot-loader/patch' import ReactReconciler from 'react-dom/lib/ReactReconciler' - -const { __NEXT_DATA__: { errorComponent } } = window -const ErrorComponent = evalScript(errorComponent).default - -require('react-hot-loader/patch') +import initOnDemandEntries from './on-demand-entries-client' +import initWebpackHMR from './webpack-hot-middleware-client' const next = window.next = require('./') -const emitter = next.default() +next.default() + .then((emitter) => { + initOnDemandEntries() + initWebpackHMR() + + let lastScroll + + emitter.on('before-reactdom-render', ({ Component, ErrorComponent }) => { + // Remember scroll when ErrorComponent is being rendered to later restore it + if (!lastScroll && Component === ErrorComponent) { + const { pageXOffset, pageYOffset } = window + lastScroll = { + x: pageXOffset, + y: pageYOffset + } + } + }) + + emitter.on('after-reactdom-render', ({ Component, ErrorComponent }) => { + if (lastScroll && Component !== ErrorComponent) { + // Restore scroll after ErrorComponent was replaced with a page component by HMR + const { x, y } = lastScroll + window.scroll(x, y) + lastScroll = null + } + }) + }) + .catch((err) => { + console.error(`${err.message}\n${err.stack}`) + }) // This is a patch to catch most of the errors throw inside React components. const originalMountComponent = ReactReconciler.mountComponent @@ -21,25 +47,3 @@ ReactReconciler.mountComponent = function (...args) { throw err } } - -let lastScroll - -emitter.on('before-reactdom-render', ({ Component }) => { - // Remember scroll when ErrorComponent is being rendered to later restore it - if (!lastScroll && Component === ErrorComponent) { - const { pageXOffset, pageYOffset } = window - lastScroll = { - x: pageXOffset, - y: pageYOffset - } - } -}) - -emitter.on('after-reactdom-render', ({ Component }) => { - if (lastScroll && Component !== ErrorComponent) { - // Restore scroll after ErrorComponent was replaced with a page component by HMR - const { x, y } = lastScroll - window.scroll(x, y) - lastScroll = null - } -}) diff --git a/client/next.js b/client/next.js index 5400a409..25654b7e 100644 --- a/client/next.js +++ b/client/next.js @@ -1,3 +1,6 @@ import next from './' next() + .catch((err) => { + console.error(`${err.message}\n${err.stack}`) + }) diff --git a/client/on-demand-entries-client.js b/client/on-demand-entries-client.js index 7fcd8c18..3bd379dc 100644 --- a/client/on-demand-entries-client.js +++ b/client/on-demand-entries-client.js @@ -3,31 +3,33 @@ import Router from '../lib/router' import fetch from 'unfetch' -Router.ready(() => { - Router.router.events.on('routeChangeComplete', ping) -}) - -async function ping () { - try { - const url = `/_next/on-demand-entries-ping?page=${Router.pathname}` - const res = await fetch(url) - const payload = await res.json() - if (payload.invalid) { - location.reload() - } - } catch (err) { - console.error(`Error with on-demand-entries-ping: ${err.message}`) - } -} - -async function runPinger () { - while (true) { - await new Promise((resolve) => setTimeout(resolve, 5000)) - await ping() - } -} - -runPinger() - .catch((err) => { - console.error(err) +export default () => { + Router.ready(() => { + Router.router.events.on('routeChangeComplete', ping) }) + + async function ping () { + try { + const url = `/_next/on-demand-entries-ping?page=${Router.pathname}` + const res = await fetch(url) + const payload = await res.json() + if (payload.invalid) { + location.reload() + } + } catch (err) { + console.error(`Error with on-demand-entries-ping: ${err.message}`) + } + } + + async function runPinger () { + while (true) { + await new Promise((resolve) => setTimeout(resolve, 5000)) + await ping() + } + } + + runPinger() + .catch((err) => { + console.error(err) + }) +} diff --git a/client/webpack-hot-middleware-client.js b/client/webpack-hot-middleware-client.js index 6f1ea9e6..f97c5853 100644 --- a/client/webpack-hot-middleware-client.js +++ b/client/webpack-hot-middleware-client.js @@ -1,48 +1,50 @@ import webpackHotMiddlewareClient from 'webpack-hot-middleware/client?overlay=false&reload=true&path=/_next/webpack-hmr' import Router from '../lib/router' -const handlers = { - reload (route) { - if (route === '/_error') { - for (const r of Object.keys(Router.components)) { - const { err } = Router.components[r] - if (err) { - // reload all error routes - // which are expected to be errors of '/_error' routes - Router.reload(r) +export default () => { + const handlers = { + reload (route) { + if (route === '/_error') { + for (const r of Object.keys(Router.components)) { + const { err } = Router.components[r] + if (err) { + // reload all error routes + // which are expected to be errors of '/_error' routes + Router.reload(r) + } } + return } - return - } - if (route === '/_document') { - window.location.reload() - return - } + if (route === '/_document') { + window.location.reload() + return + } - Router.reload(route) - }, - - change (route) { - if (route === '/_document') { - window.location.reload() - return - } - - const { err } = Router.components[route] || {} - if (err) { - // reload to recover from runtime errors Router.reload(route) + }, + + change (route) { + if (route === '/_document') { + window.location.reload() + return + } + + const { err } = Router.components[route] || {} + if (err) { + // reload to recover from runtime errors + Router.reload(route) + } } } -} -webpackHotMiddlewareClient.subscribe((obj) => { - const fn = handlers[obj.action] - if (fn) { - const data = obj.data || [] - fn(...data) - } else { - throw new Error('Unexpected action ' + obj.action) - } -}) + webpackHotMiddlewareClient.subscribe((obj) => { + const fn = handlers[obj.action] + if (fn) { + const data = obj.data || [] + fn(...data) + } else { + throw new Error('Unexpected action ' + obj.action) + } + }) +} diff --git a/lib/error.js b/lib/error.js index 1b489083..9d22e9e4 100644 --- a/lib/error.js +++ b/lib/error.js @@ -3,8 +3,8 @@ 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) + static getInitialProps ({ res, err }) { + const statusCode = res ? res.statusCode : (err ? err.statusCode : null) return { statusCode } } diff --git a/lib/eval-script.js b/lib/eval-script.js deleted file mode 100644 index 031d53e4..00000000 --- a/lib/eval-script.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * IMPORTANT: This module is compiled *without* `use strict` - * so that when we `eval` a dependency below, we don't enforce - * `use strict` implicitly. - * - * Otherwise, modules like `d3` get `eval`d and forced into - * `use strict` where they don't work (at least in current versions) - * - * To see the compilation details, look at `flyfile.js` and the - * usage of `babel-plugin-transform-remove-strict-mode`. - */ - -export default function evalScript (script) { - const module = { exports: {} } - - eval(script) // eslint-disable-line no-eval - return module.exports -} diff --git a/lib/page-loader.js b/lib/page-loader.js new file mode 100644 index 00000000..6ec0c2bc --- /dev/null +++ b/lib/page-loader.js @@ -0,0 +1,102 @@ +/* global window, document */ +import mitt from 'mitt' + +const webpackModule = module + +export default class PageLoader { + constructor (buildId, assetPrefix) { + this.buildId = buildId + this.assetPrefix = assetPrefix + + this.pageCache = {} + this.pageLoadedHandlers = {} + this.registerEvents = mitt() + this.loadingRoutes = {} + } + + normalizeRoute (route) { + if (route[0] !== '/') { + throw new Error('Route name should start with a "/"') + } + + return route.replace(/index$/, '') + } + + 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) => { + 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) { + route = this.normalizeRoute(route) + + const script = document.createElement('script') + const url = `${this.assetPrefix}/_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, regFn) { + const register = () => { + const { error, page } = regFn() + this.pageCache[route] = { error, page } + this.registerEvents.emit(route, { error, page }) + } + + // 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') { + console.log(`Waiting webpack to became "idle" to initialize the page: "${route}"`) + + const check = (status) => { + if (status === 'idle') { + webpackModule.hot.removeStatusHandler(check) + register() + } + } + webpackModule.hot.status(check) + } else { + register() + } + } + + 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 cf3197c0..8b1a4263 100644 --- a/lib/router/router.js +++ b/lib/router/router.js @@ -1,28 +1,28 @@ 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' 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) // 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() + this.pageLoader = pageLoader this.prefetchQueue = new PQueue({ concurrency: 2 }) this.ErrorComponent = ErrorComponent this.pathname = pathname @@ -77,7 +77,7 @@ export default class Router { async reload (route) { delete this.components[route] - delete this.fetchingRoutes[route] + this.pageLoader.clearCache(route) if (route !== this.route) return @@ -186,11 +186,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 +199,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.statusCode === 404) { + // 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 +282,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 +294,7 @@ export default class Router { this.componentLoadCancel = null } - return newData + return Component } async getInitialProps (Component, ctx) { @@ -324,24 +317,8 @@ export default class Router { return props } - fetchRoute (route) { - let promise = this.fetchingRoutes[route] - if (!promise) { - promise = this.fetchingRoutes[route] = this.doFetchRoute(route) - } - - 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' } - }) + async fetchRoute (route) { + return await this.pageLoader.loadPage(route) } abortComponentLoad (as) { @@ -365,22 +342,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/readme.md b/readme.md index 7a731a54..bc2e0439 100644 --- a/readme.md +++ b/readme.md @@ -34,6 +34,7 @@ Next.js is a minimalistic framework for server-rendered React applications. - [Custom configuration](#custom-configuration) - [Customizing webpack config](#customizing-webpack-config) - [Customizing babel config](#customizing-babel-config) + - [CDN support with Asset Prefix](#cdn-support-with-asset-prefix) - [Production deployment](#production-deployment) - [FAQ](#faq) - [Contributing](#contributing) @@ -704,6 +705,20 @@ Here's an example `.babelrc` file: } ``` +### CDN support with Asset Prefix + +To set up a CDN, you can set up the `assetPrefix` setting and configure your CDN's origin to resolve to the domain that Next.js is hosted on. + +```js +const isProd = process.NODE_ENV === 'production' +module.exports = { + // You may only need to add assetPrefix in the production. + assetPrefix: isProd ? 'https://cdn.mydomain.com' : '' +} +``` + +Note: Next.js will automatically use that prefix the scripts it loads, but this has no effect whatsoever on `/static`. If you want to serve those assets over the CDN, you'll have to introduce the prefix yourself. One way of introducing a prefix that works inside your components and varies by environment is documented [in this example](https://github.com/zeit/next.js/tree/master/examples/with-universal-configuration). + ## Production deployment To deploy, instead of running `next`, you want to build for production usage ahead of time. Therefore, building and starting are separate commands: diff --git a/server/build/plugins/json-pages-plugin.js b/server/build/plugins/json-pages-plugin.js deleted file mode 100644 index 06e34851..00000000 --- a/server/build/plugins/json-pages-plugin.js +++ /dev/null @@ -1,24 +0,0 @@ -export default class JsonPagesPlugin { - apply (compiler) { - compiler.plugin('after-compile', (compilation, callback) => { - const pages = Object - .keys(compilation.assets) - .filter((filename) => /^bundles[/\\]pages.*\.js$/.test(filename)) - - pages.forEach((pageName) => { - const page = compilation.assets[pageName] - delete compilation.assets[pageName] - - const content = page.source() - const newContent = JSON.stringify({ component: content }) - - compilation.assets[`${pageName}on`] = { - source: () => newContent, - size: () => newContent.length - } - }) - - callback() - }) - } -} diff --git a/server/build/plugins/pages-plugin.js b/server/build/plugins/pages-plugin.js new file mode 100644 index 00000000..203fbe60 --- /dev/null +++ b/server/build/plugins/pages-plugin.js @@ -0,0 +1,33 @@ +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 = ` + window.__NEXT_REGISTER_PAGE('${routeName}', function() { + var comp = ${content} + return { page: comp.default } + }) + ` + // Replace the exisiting chunk with the new content + compilation.assets[chunk.name] = { + source: () => newContent, + size: () => newContent.length + } + }) + callback() + }) + } +} diff --git a/server/build/webpack.js b/server/build/webpack.js index 7f950593..b0e4cb8d 100644 --- a/server/build/webpack.js +++ b/server/build/webpack.js @@ -6,7 +6,7 @@ import WriteFilePlugin from 'write-file-webpack-plugin' 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' @@ -114,7 +114,7 @@ export default async function createCompiler (dir, { dev = false, quiet = false, new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production') }), - new JsonPagesPlugin(), + new PagesPlugin(), new CaseSensitivePathPlugin() ] diff --git a/server/config.js b/server/config.js index 5a70252c..bc5059f5 100644 --- a/server/config.js +++ b/server/config.js @@ -6,7 +6,8 @@ const cache = new Map() const defaultConfig = { webpack: null, poweredByHeader: true, - distDir: '.next' + distDir: '.next', + assetPrefix: '' } export default function getConfig (dir) { diff --git a/server/document.js b/server/document.js index 469c1223..c7d4108d 100644 --- a/server/document.js +++ b/server/document.js @@ -34,9 +34,45 @@ export class Head extends Component { _documentProps: PropTypes.any } + getChunkPreloadLink (filename) { + const { __NEXT_DATA__ } = this.context._documentProps + let { buildStats, assetPrefix } = __NEXT_DATA__ + const hash = buildStats ? buildStats[filename].hash : '-' + + return ( + + ) + } + + getPreloadMainLinks () { + const { dev } = this.context._documentProps + if (dev) { + return [ + this.getChunkPreloadLink('manifest.js'), + this.getChunkPreloadLink('commons.js'), + this.getChunkPreloadLink('main.js') + ] + } + + // In the production mode, we have a single asset with all the JS content. + return [ + this.getChunkPreloadLink('app.js') + ] + } + render () { - const { head, styles } = this.context._documentProps + const { head, styles, __NEXT_DATA__ } = this.context._documentProps + const { pathname, buildId, assetPrefix } = __NEXT_DATA__ + return + + + {this.getPreloadMainLinks()} {(head || []).map((h, i) => React.cloneElement(h, { key: i }))} {styles || null} {this.props.children} @@ -67,13 +103,13 @@ export class NextScript extends Component { getChunkScript (filename, additionalProps = {}) { const { __NEXT_DATA__ } = this.context._documentProps - let { buildStats } = __NEXT_DATA__ + let { buildStats, assetPrefix } = __NEXT_DATA__ const hash = buildStats ? buildStats[filename].hash : '-' return (